diff --git a/README.md b/README.md index 6d4a8579..3be5f9b3 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ You can also check out the website for Micro at https://micro-editor.github.io. - 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 diff --git a/assets/packaging/micro.desktop b/assets/packaging/micro.desktop index 35772518..3066f82e 100644 --- a/assets/packaging/micro.desktop +++ b/assets/packaging/micro.desktop @@ -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; diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index 3e6756bc..e0ef0987 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -449,6 +449,10 @@ func DoEvent() { os.Exit(0) } + if event == nil { + return + } + if e, ok := event.(*tcell.EventError); ok { log.Println("tcell event error: ", e.Error()) @@ -469,12 +473,11 @@ func DoEvent() { } ulua.Lock.Lock() - // if event != nil { - if action.InfoBar.HasPrompt { + _, resize := event.(*tcell.EventResize) + if action.InfoBar.HasPrompt && !resize { action.InfoBar.HandleEvent(event) } else { action.Tabs.HandleEvent(event) } - // } ulua.Lock.Unlock() } diff --git a/data/micro.json b/data/micro.json index 63ba098d..49fdc662 100644 --- a/data/micro.json +++ b/data/micro.json @@ -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", diff --git a/internal/action/actions.go b/internal/action/actions.go index 74e701f4..620e8516 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -51,53 +51,47 @@ func (h *BufPane) ScrollAdjust() { func (h *BufPane) MousePress(e *tcell.EventMouse) bool { b := h.Buf mx, my := e.Position() + // ignore click on the status line + if my >= h.BufView().Y+h.BufView().Height { + return false + } mouseLoc := h.LocFromVisual(buffer.Loc{mx, my}) h.Cursor.Loc = mouseLoc - if h.mouseReleased { - if b.NumCursors() > 1 { - b.ClearCursors() - h.Relocate() - h.Cursor = h.Buf.GetActiveCursor() - h.Cursor.Loc = mouseLoc - } - if time.Since(h.lastClickTime)/time.Millisecond < config.DoubleClickThreshold && (mouseLoc.X == h.lastLoc.X && mouseLoc.Y == h.lastLoc.Y) { - if h.doubleClick { - // Triple click - h.lastClickTime = time.Now() - h.tripleClick = true - h.doubleClick = false - - h.Cursor.SelectLine() - h.Cursor.CopySelection(clipboard.PrimaryReg) - } else { - // Double click - h.lastClickTime = time.Now() - - h.doubleClick = true - h.tripleClick = false - - h.Cursor.SelectWord() - h.Cursor.CopySelection(clipboard.PrimaryReg) - } - } else { - h.doubleClick = false - h.tripleClick = false + if b.NumCursors() > 1 { + b.ClearCursors() + h.Relocate() + h.Cursor = h.Buf.GetActiveCursor() + h.Cursor.Loc = mouseLoc + } + if time.Since(h.lastClickTime)/time.Millisecond < config.DoubleClickThreshold && (mouseLoc.X == h.lastLoc.X && mouseLoc.Y == h.lastLoc.Y) { + if h.doubleClick { + // Triple click h.lastClickTime = time.Now() - h.Cursor.OrigSelection[0] = h.Cursor.Loc - h.Cursor.CurSelection[0] = h.Cursor.Loc - h.Cursor.CurSelection[1] = h.Cursor.Loc - } - h.mouseReleased = false - } else if !h.mouseReleased { - if h.tripleClick { - h.Cursor.AddLineToSelection() - } else if h.doubleClick { - h.Cursor.AddWordToSelection() + h.tripleClick = true + h.doubleClick = false + + h.Cursor.SelectLine() + h.Cursor.CopySelection(clipboard.PrimaryReg) } else { - h.Cursor.SetSelectionEnd(h.Cursor.Loc) + // Double click + h.lastClickTime = time.Now() + + h.doubleClick = true + h.tripleClick = false + + h.Cursor.SelectWord() + h.Cursor.CopySelection(clipboard.PrimaryReg) } + } else { + h.doubleClick = false + h.tripleClick = false + h.lastClickTime = time.Now() + + h.Cursor.OrigSelection[0] = h.Cursor.Loc + h.Cursor.CurSelection[0] = h.Cursor.Loc + h.Cursor.CurSelection[1] = h.Cursor.Loc } h.Cursor.StoreVisualX() @@ -106,6 +100,45 @@ func (h *BufPane) MousePress(e *tcell.EventMouse) bool { return true } +func (h *BufPane) MouseDrag(e *tcell.EventMouse) bool { + mx, my := e.Position() + // ignore drag on the status line + if my >= h.BufView().Y+h.BufView().Height { + return false + } + h.Cursor.Loc = h.LocFromVisual(buffer.Loc{mx, my}) + + if h.tripleClick { + h.Cursor.AddLineToSelection() + } else if h.doubleClick { + h.Cursor.AddWordToSelection() + } else { + h.Cursor.SetSelectionEnd(h.Cursor.Loc) + } + + h.Cursor.StoreVisualX() + h.Relocate() + return true +} + +func (h *BufPane) MouseRelease(e *tcell.EventMouse) bool { + // We could finish the selection based on the release location as in the + // commented out code below, to allow text selections even in a terminal + // that doesn't support mouse motion events. But when the mouse click is + // within the scroll margin, that would cause a scroll and selection + // even for a simple mouse click, which is not good. + // if !h.doubleClick && !h.tripleClick { + // mx, my := e.Position() + // h.Cursor.Loc = h.LocFromVisual(buffer.Loc{mx, my}) + // h.Cursor.SetSelectionEnd(h.Cursor.Loc) + // } + + if h.Cursor.HasSelection() { + h.Cursor.CopySelection(clipboard.PrimaryReg) + } + return true +} + // ScrollUpAction scrolls the view up func (h *BufPane) ScrollUpAction() bool { h.ScrollUp(util.IntOpt(h.Buf.Settings["scrollspeed"])) @@ -211,7 +244,7 @@ func (h *BufPane) CursorLeft() bool { func (h *BufPane) CursorRight() bool { if h.Cursor.HasSelection() { h.Cursor.Deselect(false) - h.Cursor.Loc = h.Cursor.Loc.Move(1, h.Buf) + h.Cursor.Right() } else { tabstospaces := h.Buf.Settings["tabstospaces"].(bool) tabmovement := h.Buf.Settings["tabmovement"].(bool) @@ -793,25 +826,26 @@ func (h *BufPane) SaveAsCB(action string, callback func()) bool { filename := strings.Join(args, " ") fileinfo, err := os.Stat(filename) if err != nil { - if os.IsNotExist(err) { + if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrPermission) { noPrompt := h.saveBufToFile(filename, action, callback) if noPrompt { h.completeAction(action) return } } - } - InfoBar.YNPrompt( - fmt.Sprintf("the file %s already exists in the directory, would you like to overwrite? Y/n", fileinfo.Name()), - func(yes, canceled bool) { - if yes && !canceled { - noPrompt := h.saveBufToFile(filename, action, callback) - if noPrompt { - h.completeAction(action) + } else { + InfoBar.YNPrompt( + fmt.Sprintf("The file %s already exists in the directory, would you like to overwrite? Y/n", fileinfo.Name()), + func(yes, canceled bool) { + if yes && !canceled { + noPrompt := h.saveBufToFile(filename, action, callback) + if noPrompt { + h.completeAction(action) + } } - } - }, - ) + }, + ) + } } }) return false @@ -1273,9 +1307,13 @@ func (h *BufPane) PastePrimary() bool { func (h *BufPane) paste(clip string) { if h.Buf.Settings["smartpaste"].(bool) { - if h.Cursor.X > 0 && len(util.GetLeadingWhitespace([]byte(strings.TrimLeft(clip, "\r\n")))) == 0 { - leadingWS := util.GetLeadingWhitespace(h.Buf.LineBytes(h.Cursor.Y)) - clip = strings.ReplaceAll(clip, "\n", "\n"+string(leadingWS)) + if h.Cursor.X > 0 { + leadingPasteWS := string(util.GetLeadingWhitespace([]byte(clip))) + if leadingPasteWS != " " && strings.Contains(clip, "\n"+leadingPasteWS) { + leadingWS := string(util.GetLeadingWhitespace(h.Buf.LineBytes(h.Cursor.Y))) + clip = strings.TrimPrefix(clip, leadingPasteWS) + clip = strings.ReplaceAll(clip, "\n"+leadingPasteWS, "\n"+leadingWS) + } } } @@ -1711,7 +1749,7 @@ func (h *BufPane) PlayMacro() bool { switch t := action.(type) { case rune: h.DoRuneInsert(t) - case func(*BufPane) bool: + case BufKeyAction: t(h) } } @@ -1760,15 +1798,39 @@ func (h *BufPane) SpawnMultiCursor() bool { return true } -// SpawnMultiCursorUp creates additional cursor, at the same X (if possible), one Y less. -func (h *BufPane) SpawnMultiCursorUp() bool { - if h.Cursor.Y == 0 { - return false - } - h.Cursor.GotoLoc(buffer.Loc{h.Cursor.X, h.Cursor.Y - 1}) - h.Cursor.Relocate() +// SpawnMultiCursorUpN is not an action +func (h *BufPane) SpawnMultiCursorUpN(n int) bool { + lastC := h.Buf.GetCursor(h.Buf.NumCursors() - 1) + var c *buffer.Cursor + if !h.Buf.Settings["softwrap"].(bool) { + if n > 0 && lastC.Y == 0 { + return false + } + if n < 0 && lastC.Y+1 == h.Buf.LinesNum() { + return false + } + + h.Buf.DeselectCursors() + + c = buffer.NewCursor(h.Buf, buffer.Loc{lastC.X, lastC.Y - n}) + c.LastVisualX = lastC.LastVisualX + c.X = c.GetCharPosInLine(h.Buf.LineBytes(c.Y), c.LastVisualX) + c.Relocate() + } else { + vloc := h.VLocFromLoc(lastC.Loc) + sloc := h.Scroll(vloc.SLoc, -n) + if sloc == vloc.SLoc { + return false + } + + h.Buf.DeselectCursors() + + vloc.SLoc = sloc + vloc.VisualX = lastC.LastVisualX + c = buffer.NewCursor(h.Buf, h.LocFromVLoc(vloc)) + c.LastVisualX = lastC.LastVisualX + } - c := buffer.NewCursor(h.Buf, buffer.Loc{h.Cursor.X, h.Cursor.Y + 1}) h.Buf.AddCursor(c) h.Buf.SetCurCursor(h.Buf.NumCursors() - 1) h.Buf.MergeCursors() @@ -1777,20 +1839,14 @@ func (h *BufPane) SpawnMultiCursorUp() bool { return true } +// SpawnMultiCursorUp creates additional cursor, at the same X (if possible), one Y less. +func (h *BufPane) SpawnMultiCursorUp() bool { + return h.SpawnMultiCursorUpN(1) +} + // SpawnMultiCursorDown creates additional cursor, at the same X (if possible), one Y more. func (h *BufPane) SpawnMultiCursorDown() bool { - if h.Cursor.Y+1 == h.Buf.LinesNum() { - return false - } - h.Cursor.GotoLoc(buffer.Loc{h.Cursor.X, h.Cursor.Y + 1}) - h.Cursor.Relocate() - - c := buffer.NewCursor(h.Buf, buffer.Loc{h.Cursor.X, h.Cursor.Y - 1}) - h.Buf.AddCursor(c) - h.Buf.SetCurCursor(h.Buf.NumCursors() - 1) - h.Buf.MergeCursors() - h.Relocate() - return true + return h.SpawnMultiCursorUpN(-1) } // SpawnMultiCursorSelect adds a cursor at the beginning of each line of a selection @@ -1827,11 +1883,27 @@ func (h *BufPane) SpawnMultiCursorSelect() bool { return true } -// MouseMultiCursor is a mouse action which puts a new cursor at the mouse position +// MouseMultiCursor is a mouse action which puts a new cursor at the mouse position, +// or removes a cursor if it is already there func (h *BufPane) MouseMultiCursor(e *tcell.EventMouse) bool { b := h.Buf mx, my := e.Position() + // ignore click on the status line + if my >= h.BufView().Y+h.BufView().Height { + return false + } mouseLoc := h.LocFromVisual(buffer.Loc{X: mx, Y: my}) + + if h.Buf.NumCursors() > 1 { + cursors := h.Buf.GetCursors() + for _, c := range cursors { + if c.Loc == mouseLoc { + h.Buf.RemoveCursor(c.Num) + return true + } + } + } + c := buffer.NewCursor(b, mouseLoc) b.AddCursor(c) b.MergeCursors() diff --git a/internal/action/bindings.go b/internal/action/bindings.go index 846e3d11..e2bf10a0 100644 --- a/internal/action/bindings.go +++ b/internal/action/bindings.go @@ -201,11 +201,20 @@ modSearch: }, 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 } diff --git a/internal/action/bufpane.go b/internal/action/bufpane.go index dea7b906..200a972d 100644 --- a/internal/action/bufpane.go +++ b/internal/action/bufpane.go @@ -8,7 +8,6 @@ import ( 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" @@ -17,6 +16,8 @@ import ( "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++ { @@ -102,7 +113,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 +124,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,13 +140,16 @@ 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 { + bufAction := func(h *BufPane, te *tcell.EventMouse) bool { cursors := h.Buf.GetCursors() success := true for i, a := range actionfns { @@ -147,7 +161,7 @@ func bufMapKey(k Event, action string) { 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) + innerSuccess = innerSuccess && h.execAction(a, names[i], j, te) } else { break } @@ -159,17 +173,13 @@ func bufMapKey(k Event, action string) { 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,11 +210,15 @@ 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 @@ -250,7 +264,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 } @@ -322,6 +336,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,7 +352,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 + h.resetMouse() // Set isOverwriteMode to false, because we assume we are in the default // mode when editor is opened h.isOverwriteMode = false @@ -446,50 +466,32 @@ func (h *BufPane) HandleEvent(event tcell.Event) { 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. + for me := range h.mousePressed { + delete(h.mousePressed, me) + + me.state = MouseRelease + h.DoMouseEvent(me, e) + } } } h.Buf.MergeCursors() @@ -509,6 +511,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. @@ -534,7 +544,7 @@ 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, cursor int, te *tcell.EventMouse) bool { if name != "Autocomplete" && name != "CycleAutocompleteBack" { h.Buf.HasSuggestions = false } @@ -542,7 +552,13 @@ func (h *BufPane) execAction(action func(*BufPane) bool, name string, cursor int _, isMulti := MultiActions[name] if (!isMulti && cursor == 0) || isMulti { if h.PluginCB("pre" + name) { - success := action(h) + 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 isMulti { @@ -804,6 +820,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, } diff --git a/internal/action/command.go b/internal/action/command.go index 16d2fff8..23d5bb93 100644 --- a/internal/action/command.go +++ b/internal/action/command.go @@ -375,7 +375,7 @@ func (h *BufPane) ReopenCmd(args []string) { func (h *BufPane) openHelp(page string) 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) @@ -532,8 +532,12 @@ func SetGlobalOptionNative(option string, nativeValue interface{}) error { } } - for _, b := range buffer.OpenBuffers { - b.SetOptionNative(option, nativeValue) + if local { + MainTab().CurPane().Buf.SetOptionNative(option, nativeValue) + } else { + for _, b := range buffer.OpenBuffers { + b.SetOptionNative(option, nativeValue) + } } return config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json")) @@ -697,6 +701,34 @@ func (h *BufPane) QuitCmd(args []string) { h.Quit() } +func convertLine(h *BufPane, line string) (int, error) { + lineNum := 0 + var err error + + // Check for special negative movement beginning from the end of the file + if strings.HasPrefix(line, "~") { + lineNum, err = strconv.Atoi(line[1:]) + if err != nil { + return 0, err + } + lineNum = h.Buf.LinesNum() + 1 - lineNum + } else { + lineNum, err = strconv.Atoi(line) + if err != nil { + return 0, err + } + + // Check for relative numbers + if strings.HasPrefix(line, "-") || strings.HasPrefix(line, "+") { + lineNum = h.Buf.GetActiveCursor().Y + 1 + lineNum + } + } + + lineNum = util.Clamp(lineNum-1, 0, h.Buf.LinesNum()-1) + + return lineNum, err +} + // GotoCmd is a command that will send the cursor to a certain // position in the buffer // For example: `goto line`, or `goto line:col` @@ -704,37 +736,30 @@ func (h *BufPane) GotoCmd(args []string) { if len(args) <= 0 { InfoBar.Error("Not enough arguments") } else { + line, col := 0, 0 + var err error h.RemoveAllMultiCursors() if strings.Contains(args[0], ":") { parts := strings.SplitN(args[0], ":", 2) - line, err := strconv.Atoi(parts[0]) + line, err = convertLine(h, parts[0]) if err != nil { InfoBar.Error(err) return } - col, err := strconv.Atoi(parts[1]) + 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]) + line, err = convertLine(h, 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}) } + h.GotoLoc(buffer.Loc{col, line}) } } diff --git a/internal/action/defaults_darwin.go b/internal/action/defaults_darwin.go index 24b1f060..e1a54b79 100644 --- a/internal/action/defaults_darwin.go +++ b/internal/action/defaults_darwin.go @@ -92,11 +92,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", @@ -175,8 +177,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", } diff --git a/internal/action/defaults_other.go b/internal/action/defaults_other.go index 2edc48e0..a932688a 100644 --- a/internal/action/defaults_other.go +++ b/internal/action/defaults_other.go @@ -95,11 +95,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", @@ -178,8 +180,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", } diff --git a/internal/action/events.go b/internal/action/events.go index 3a4834f6..4addf1b5 100644 --- a/internal/action/events.go +++ b/internal/action/events.go @@ -100,11 +100,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 +131,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 "" diff --git a/internal/action/infocomplete.go b/internal/action/infocomplete.go index 12f7844b..e093123d 100644 --- a/internal/action/infocomplete.go +++ b/internal/action/infocomplete.go @@ -17,7 +17,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 +38,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,6 +77,24 @@ func colorschemeComplete(input string) (string, []string) { return chosen, suggestions } +// filetypeComplete autocompletes filetype +func filetypeComplete(input string) (string, []string) { + var suggestions []string + + for _, f := range config.ListRuntimeFiles(config.RTSyntax) { + if strings.HasPrefix(f.Name(), input) { + suggestions = append(suggestions, f.Name()) + } + } + + var chosen string + if len(suggestions) == 1 { + chosen = suggestions[0] + } + + return chosen, suggestions +} + func contains(s []string, e string) bool { for _, a := range s { if a == e { @@ -89,7 +107,7 @@ func contains(s []string, e string) bool { // 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 +134,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,6 +190,8 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) { switch inputOpt { case "colorscheme": _, suggestions = colorschemeComplete(input) + case "filetype": + _, suggestions = filetypeComplete(input) case "fileformat": if strings.HasPrefix("unix", input) { suggestions = append(suggestions, "unix") @@ -196,6 +216,13 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) { if strings.HasPrefix("terminal", input) { suggestions = append(suggestions, "terminal") } + case "matchbracestyle": + if strings.HasPrefix("underline", input) { + suggestions = append(suggestions, "underline") + } + if strings.HasPrefix("highlight", input) { + suggestions = append(suggestions, "highlight") + } } } sort.Strings(suggestions) @@ -210,7 +237,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 +259,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{' '}) diff --git a/internal/action/tab.go b/internal/action/tab.go index 600ccc55..357eefbf 100644 --- a/internal/action/tab.go +++ b/internal/action/tab.go @@ -164,6 +164,21 @@ func InitTabs(bufs []*buffer.Buffer) { } } } + + screen.RestartCallback = func() { + // The mouse could be released after the screen was stopped, so that + // we couldn't catch the mouse release event and would erroneously think + // that it is still pressed. So need to reset the mouse release state + // after the screen is restarted. + for _, t := range Tabs.List { + t.release = true + for _, p := range t.Panes { + if bp, ok := p.(*BufPane); ok { + bp.resetMouse() + } + } + } + } } func MainTab() *Tab { @@ -214,34 +229,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 +272,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 diff --git a/internal/buffer/autocomplete.go b/internal/buffer/autocomplete.go index 414e3060..7a8c5bee 100644 --- a/internal/buffer/autocomplete.go +++ b/internal/buffer/autocomplete.go @@ -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) @@ -83,7 +83,7 @@ func GetWord(b *Buffer) ([]byte, int) { } // 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,7 +104,7 @@ 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) @@ -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{} diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index bd172d58..f7181e99 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -568,6 +568,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) @@ -1056,7 +1063,7 @@ func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool, boo leftChar = curLine[start.X-1] } var i int - if startChar == braceType[0] || leftChar == braceType[0] { + if startChar == braceType[0] || (leftChar == braceType[0] && startChar != braceType[1]) { for y := start.Y; y < b.LinesNum(); y++ { l := []rune(string(b.LineBytes(y))) xInit := 0 @@ -1087,24 +1094,24 @@ func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool, boo l := []rune(string(b.lines[y].data)) xInit := len(l) - 1 if y == start.Y { - if leftChar == braceType[1] { - xInit = start.X - 1 - } else { + if startChar == braceType[1] { xInit = start.X + } else { + xInit = start.X - 1 } } 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 + if startChar == braceType[1] { + return Loc{x, y}, false, true } - return Loc{x, y}, false, true + return Loc{x, y}, true, true } - } else if r == braceType[1] { - i++ } } } diff --git a/internal/buffer/cursor.go b/internal/buffer/cursor.go index 12fc5db2..bd3ae068 100644 --- a/internal/buffer/cursor.go +++ b/internal/buffer/cursor.go @@ -30,6 +30,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 +43,8 @@ func NewCursor(b *Buffer, l Loc) *Cursor { c := &Cursor{ buf: b, Loc: l, + + NewTrailingWsY: -1, } c.StoreVisualX() return c diff --git a/internal/buffer/eventhandler.go b/internal/buffer/eventhandler.go index 6be34bce..53f64025 100644 --- a/internal/buffer/eventhandler.go +++ b/internal/buffer/eventhandler.go @@ -106,6 +106,10 @@ func (eh *EventHandler) DoTextEvent(t *TextEvent, useUndo bool) { c.Relocate() c.LastVisualX = c.GetVisualX() } + + if useUndo { + eh.updateTrailingWs(t) + } } // ExecuteTextEvent runs a text event @@ -290,6 +294,7 @@ func (eh *EventHandler) UndoOneEvent() { if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) { t.C = *eh.cursors[teCursor.Num] eh.cursors[teCursor.Num].Goto(teCursor) + eh.cursors[teCursor.Num].NewTrailingWsY = teCursor.NewTrailingWsY } else { teCursor.Num = -1 } @@ -333,6 +338,7 @@ func (eh *EventHandler) RedoOneEvent() { if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) { t.C = *eh.cursors[teCursor.Num] eh.cursors[teCursor.Num].Goto(teCursor) + eh.cursors[teCursor.Num].NewTrailingWsY = teCursor.NewTrailingWsY } else { teCursor.Num = -1 } @@ -342,3 +348,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 + } +} diff --git a/internal/buffer/save.go b/internal/buffer/save.go index 6829d4a4..0a10a2dc 100644 --- a/internal/buffer/save.go +++ b/internal/buffer/save.go @@ -54,7 +54,7 @@ func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error, screen.TempStart(screenb) return err } - } else if writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil { + } else if writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666); err != nil { return } diff --git a/internal/config/rtfiles.go b/internal/config/rtfiles.go index fb4497e6..275831cf 100644 --- a/internal/config/rtfiles.go +++ b/internal/config/rtfiles.go @@ -129,9 +129,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))) + af := assetFile(path.Join(directory, f)) + for _, rf := range realFiles[fileType] { + if af.Name() == rf.Name() { + continue assetLoop + } + } + AddRuntimeFile(fileType, af) } } } @@ -218,7 +226,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 diff --git a/internal/config/settings.go b/internal/config/settings.go index eca52074..ffa39ebb 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "reflect" + "runtime" "strconv" "strings" @@ -42,18 +43,19 @@ func init() { // Options with validators var optionValidators = map[string]optionValidator{ - "autosave": validateNonNegativeValue, - "clipboard": validateClipboard, - "detectlimit": validateNonNegativeValue, - "tabsize": validatePositiveValue, - "scrollmargin": validateNonNegativeValue, - "scrollspeed": validateNonNegativeValue, - "colorscheme": validateColorscheme, - "colorcolumn": validateNonNegativeValue, - "fileformat": validateLineEnding, - "encoding": validateEncoding, - "multiopen": validateMultiOpen, - "reload": validateReload, + "autosave": validateNonNegativeValue, + "clipboard": validateClipboard, + "detectlimit": validateNonNegativeValue, + "tabsize": validatePositiveValue, + "scrollmargin": validateNonNegativeValue, + "scrollspeed": validateNonNegativeValue, + "colorscheme": validateColorscheme, + "colorcolumn": validateNonNegativeValue, + "fileformat": validateLineEnding, + "encoding": validateEncoding, + "multiopen": validateMultiOpen, + "reload": validateReload, + "matchbracestyle": validateMatchBraceStyle, } func ReadSettings() error { @@ -274,51 +276,61 @@ func GetGlobalOption(name string) interface{} { } var defaultCommonSettings = map[string]interface{}{ - "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": "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, + "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, + "incsearch": true, + "ignorecase": true, + "indentchar": " ", + "keepautoindent": false, + "matchbrace": true, + "matchbracestyle": "underline", + "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 defaultFileFormat() string { + if runtime.GOOS == "windows" { + return "dos" + } + return "unix" } func GetInfoBarOffset() int { @@ -542,6 +554,22 @@ func validateReload(option string, value interface{}) error { case "prompt", "auto", "disabled": default: return errors.New(option + " must be 'prompt', 'auto' or 'disabled'") + } + + return nil +} + +func validateMatchBraceStyle(option string, value interface{}) error { + val, ok := value.(string) + + if !ok { + errors.New("Expected string type for matchbracestyle") + } + + switch val { + case "underline", "highlight": + default: + return errors.New(option + " must be 'underline' or 'highlight'") } return nil diff --git a/internal/display/bufwindow.go b/internal/display/bufwindow.go index fde20969..942dd167 100644 --- a/internal/display/bufwindow.go +++ b/internal/display/bufwindow.go @@ -129,6 +129,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,13 +151,13 @@ 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() @@ -277,13 +282,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 +328,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 @@ -407,7 +418,9 @@ func (w *BufWindow) displayBuffer() { if found { matchingBraces = append(matchingBraces, mb) if !left { - matchingBraces = append(matchingBraces, curLoc) + if b.Settings["matchbracestyle"].(string) != "highlight" { + matchingBraces = append(matchingBraces, curLoc) + } } else { matchingBraces = append(matchingBraces, curLoc.Move(-1, b)) } @@ -482,6 +495,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 +524,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 +607,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) + } } } } @@ -609,7 +667,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:] diff --git a/internal/display/termwindow.go b/internal/display/termwindow.go index 6c30fa7f..e5d80494 100644 --- a/internal/display/termwindow.go +++ b/internal/display/termwindow.go @@ -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) + } } } diff --git a/internal/screen/screen.go b/internal/screen/screen.go index 339e69aa..16c011e6 100644 --- a/internal/screen/screen.go +++ b/internal/screen/screen.go @@ -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 @@ -134,6 +138,10 @@ func TempStart(screenWasNil bool) { if !screenWasNil { Init() Unlock() + + if RestartCallback != nil { + RestartCallback() + } } } diff --git a/internal/util/util.go b/internal/util/util.go index 81448f0d..bebd949b 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -16,6 +16,7 @@ import ( "strings" "time" "unicode" + "unicode/utf8" "github.com/blang/semver" runewidth "github.com/mattn/go-runewidth" @@ -315,7 +316,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 { @@ -363,6 +364,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)) diff --git a/internal/views/splits.go b/internal/views/splits.go index 1168ab5d..5fb7dc65 100644 --- a/internal/views/splits.go +++ b/internal/views/splits.go @@ -185,6 +185,9 @@ 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 { + if size <= 0 { + return false + } if len(n.parent.children) <= 1 { // cannot resize a lone node return false diff --git a/runtime/colorschemes/atom-dark.micro b/runtime/colorschemes/atom-dark.micro index 3efd5d4d..0f462995 100644 --- a/runtime/colorschemes/atom-dark.micro +++ b/runtime/colorschemes/atom-dark.micro @@ -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" diff --git a/runtime/colorschemes/bubblegum.micro b/runtime/colorschemes/bubblegum.micro index af0dbce6..dcc2276d 100644 --- a/runtime/colorschemes/bubblegum.micro +++ b/runtime/colorschemes/bubblegum.micro @@ -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" diff --git a/runtime/colorschemes/cmc-16.micro b/runtime/colorschemes/cmc-16.micro index 44be786d..0a50096c 100644 --- a/runtime/colorschemes/cmc-16.micro +++ b/runtime/colorschemes/cmc-16.micro @@ -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" diff --git a/runtime/colorschemes/cmc-tc.micro b/runtime/colorschemes/cmc-tc.micro index c20e7370..b0502c6f 100644 --- a/runtime/colorschemes/cmc-tc.micro +++ b/runtime/colorschemes/cmc-tc.micro @@ -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" diff --git a/runtime/colorschemes/darcula.micro b/runtime/colorschemes/darcula.micro index f8918160..7103e842 100644 --- a/runtime/colorschemes/darcula.micro +++ b/runtime/colorschemes/darcula.micro @@ -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" diff --git a/runtime/colorschemes/default.micro b/runtime/colorschemes/default.micro index 27cba2d5..9ae5bfd6 100644 --- a/runtime/colorschemes/default.micro +++ b/runtime/colorschemes/default.micro @@ -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" diff --git a/runtime/colorschemes/dracula-tc.micro b/runtime/colorschemes/dracula-tc.micro index f87572ff..af0509c7 100644 --- a/runtime/colorschemes/dracula-tc.micro +++ b/runtime/colorschemes/dracula-tc.micro @@ -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" diff --git a/runtime/colorschemes/dukedark-tc.micro b/runtime/colorschemes/dukedark-tc.micro index 5774468b..52ec8476 100644 --- a/runtime/colorschemes/dukedark-tc.micro +++ b/runtime/colorschemes/dukedark-tc.micro @@ -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" diff --git a/runtime/colorschemes/dukelight-tc.micro b/runtime/colorschemes/dukelight-tc.micro index d3f492d2..c694ffb6 100644 --- a/runtime/colorschemes/dukelight-tc.micro +++ b/runtime/colorschemes/dukelight-tc.micro @@ -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" diff --git a/runtime/colorschemes/dukeubuntu-tc.micro b/runtime/colorschemes/dukeubuntu-tc.micro index 1a14b3f8..b34cc2c4 100644 --- a/runtime/colorschemes/dukeubuntu-tc.micro +++ b/runtime/colorschemes/dukeubuntu-tc.micro @@ -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" diff --git a/runtime/colorschemes/geany.micro b/runtime/colorschemes/geany.micro index b6851a20..43ced31a 100644 --- a/runtime/colorschemes/geany.micro +++ b/runtime/colorschemes/geany.micro @@ -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" diff --git a/runtime/colorschemes/gotham.micro b/runtime/colorschemes/gotham.micro index e8dc99c9..d067a81c 100644 --- a/runtime/colorschemes/gotham.micro +++ b/runtime/colorschemes/gotham.micro @@ -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" diff --git a/runtime/colorschemes/gruvbox-tc.micro b/runtime/colorschemes/gruvbox-tc.micro index e41b1ac2..65d72b15 100644 --- a/runtime/colorschemes/gruvbox-tc.micro +++ b/runtime/colorschemes/gruvbox-tc.micro @@ -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" diff --git a/runtime/colorschemes/gruvbox.micro b/runtime/colorschemes/gruvbox.micro index 823524ae..46cc09cf 100644 --- a/runtime/colorschemes/gruvbox.micro +++ b/runtime/colorschemes/gruvbox.micro @@ -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" diff --git a/runtime/colorschemes/material-tc.micro b/runtime/colorschemes/material-tc.micro index 7cb30658..5a7f9c89 100644 --- a/runtime/colorschemes/material-tc.micro +++ b/runtime/colorschemes/material-tc.micro @@ -30,3 +30,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" diff --git a/runtime/colorschemes/monokai-dark.micro b/runtime/colorschemes/monokai-dark.micro index 3449b305..3a1e89c4 100644 --- a/runtime/colorschemes/monokai-dark.micro +++ b/runtime/colorschemes/monokai-dark.micro @@ -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" diff --git a/runtime/colorschemes/monokai.micro b/runtime/colorschemes/monokai.micro index 4f93211a..13c44b74 100644 --- a/runtime/colorschemes/monokai.micro +++ b/runtime/colorschemes/monokai.micro @@ -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" diff --git a/runtime/colorschemes/one-dark.micro b/runtime/colorschemes/one-dark.micro index c3b0e8cd..ed994321 100644 --- a/runtime/colorschemes/one-dark.micro +++ b/runtime/colorschemes/one-dark.micro @@ -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" diff --git a/runtime/colorschemes/railscast.micro b/runtime/colorschemes/railscast.micro index 3abb1683..01c0055d 100644 --- a/runtime/colorschemes/railscast.micro +++ b/runtime/colorschemes/railscast.micro @@ -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" diff --git a/runtime/colorschemes/simple.micro b/runtime/colorschemes/simple.micro index 4ee416d4..707c04cb 100644 --- a/runtime/colorschemes/simple.micro +++ b/runtime/colorschemes/simple.micro @@ -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" diff --git a/runtime/colorschemes/solarized-tc.micro b/runtime/colorschemes/solarized-tc.micro index f2840ec3..8eae0391 100644 --- a/runtime/colorschemes/solarized-tc.micro +++ b/runtime/colorschemes/solarized-tc.micro @@ -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" diff --git a/runtime/colorschemes/solarized.micro b/runtime/colorschemes/solarized.micro index 19b8e2c3..4d3923ad 100644 --- a/runtime/colorschemes/solarized.micro +++ b/runtime/colorschemes/solarized.micro @@ -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" diff --git a/runtime/colorschemes/sunny-day.micro b/runtime/colorschemes/sunny-day.micro index f851f318..c4161bce 100644 --- a/runtime/colorschemes/sunny-day.micro +++ b/runtime/colorschemes/sunny-day.micro @@ -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-line match-brace "230,22" +color-link tab-error "210" +color-link trailingws "210" diff --git a/runtime/colorschemes/twilight.micro b/runtime/colorschemes/twilight.micro index f59d9e41..8135bb80 100644 --- a/runtime/colorschemes/twilight.micro +++ b/runtime/colorschemes/twilight.micro @@ -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" diff --git a/runtime/colorschemes/zenburn.micro b/runtime/colorschemes/zenburn.micro index ec8a9580..acbd83fb 100644 --- a/runtime/colorschemes/zenburn.micro +++ b/runtime/colorschemes/zenburn.micro @@ -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" diff --git a/runtime/help/colors.md b/runtime/help/colors.md index 4a3ee7e0..a6970d15 100644 --- a/runtime/help/colors.md +++ b/runtime/help/colors.md @@ -194,6 +194,7 @@ Here is a list of the colorscheme groups that you can use: * divider (Color of the divider between vertical splits) * message (Color of messages in the bottom line of the screen) * error-message (Color of error messages in the bottom line of the screen) +* match-brace (Color of matching brackets when `matchbracestyle` is set to `highlight`) Colorschemes must be placed in the `~/.config/micro/colorschemes` directory to be used. diff --git a/runtime/help/commands.md b/runtime/help/commands.md index afcffd29..e8630310 100644 --- a/runtime/help/commands.md +++ b/runtime/help/commands.md @@ -31,9 +31,11 @@ quotes here but these are not necessary when entering the command in micro. * `quit`: quits micro. -* `goto 'line'`: jumps to the given line number. A negative number can be - passed to jump inward from the end of the file; for example, -5 jumps - to the 5th-last line in the file. +* `goto 'line[:col]'`: Jumps to the given line (and optional column) number. + `line` can be prefixed with `+`, `-` or `~`. + +/- will perform relative jumps from the current position of the cursor + (e.g. +5 will jump 5 lines down), while ~ will perform a jump inward + from the end of the file (e.g. ~5 jumps to the 5th-last line in the file). * `replace 'search' 'value' 'flags'?`: This will replace `search` with `value`. The `flags` are optional. Possible flags are: diff --git a/runtime/help/keybindings.md b/runtime/help/keybindings.md index 9670d888..90ad0205 100644 --- a/runtime/help/keybindings.md +++ b/runtime/help/keybindings.md @@ -409,8 +409,14 @@ mouse actions) ``` MouseLeft +MouseLeftDrag +MouseLeftRelease MouseMiddle +MouseMiddleDrag +MouseMiddleRelease MouseRight +MouseRightDrag +MouseRightRelease MouseWheelUp MouseWheelDown MouseWheelLeft @@ -524,11 +530,13 @@ conventions for text editing defaults. "Esc": "Escape", // 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", // Multi-cursor bindings "Alt-n": "SpawnMultiCursor", @@ -634,10 +642,12 @@ are given below: "Esc": "AbortCommand", // Mouse bindings - "MouseWheelUp": "HistoryUp", - "MouseWheelDown": "HistoryDown", - "MouseLeft": "MousePress", - "MouseMiddle": "PastePrimary" + "MouseWheelUp": "HistoryUp", + "MouseWheelDown": "HistoryDown", + "MouseLeft": "MousePress", + "MouseLeftDrag": "MouseDrag", + "MouseLeftRelease": "MouseRelease", + "MouseMiddle": "PastePrimary" } } ``` diff --git a/runtime/help/options.md b/runtime/help/options.md index 376efdb5..be515875 100644 --- a/runtime/help/options.md +++ b/runtime/help/options.md @@ -164,7 +164,7 @@ Here are the available options: an effect if the file is empty/newly created, because otherwise the fileformat will be automatically detected from the existing line endings. - default value: `unix` + default value: `unix` on Unix systems, `dos` on Windows * `filetype`: sets the filetype for the current buffer. Set this option to `off` to completely disable filetype detection. @@ -181,6 +181,19 @@ Here are the available options: default value: `false` +* `hltaberrors`: highlight tabs when spaces are expected, and spaces when tabs + are expected. More precisely: if `tabstospaces` option is on, highlight + all tab characters; if `tabstospaces` is off, highlight space characters + in the initial indent part of the line. + + default value: `false` + +* `hltrailingws`: highlight trailing whitespaces at ends of lines. Note that + it doesn't highlight newly added trailing whitespaces that naturally occur + while typing text. It highlights only nasty forgotten trailing whitespaces. + + default value: `false` + * `incsearch`: enable incremental search in "Find" prompt (matching as you type). default value: `true` @@ -217,11 +230,19 @@ Here are the available options: default value: `false` -* `matchbrace`: underline matching braces for '()', '{}', '[]' when the cursor - is on a brace character. +* `matchbrace`: show matching braces for '()', '{}', '[]' when the cursor + is on a brace character or next to it. default value: `true` +* `matchbracestyle`: whether to underline or highlight matching braces when + `matchbrace` is enabled. The color of highlight is determined by the `match-brace` + field in the current theme. Possible values: + * `underline`: underline matching braces. + * `highlight`: use `match-brace` style from the current theme. + + default value: `underline` + * `mkparents`: if a file is opened on a path that does not exist, the file cannot be saved because the parent directories don't exist. This option lets micro automatically create the parent directories in such a situation. @@ -502,6 +523,7 @@ so that you can see what the formatting should look like. "linter": true, "literate": true, "matchbrace": true, + "matchbracestyle": "underline", "mkparents": false, "mouse": true, "parsecursor": false, diff --git a/runtime/help/plugins.md b/runtime/help/plugins.md index dc87476f..19dd0c3e 100644 --- a/runtime/help/plugins.md +++ b/runtime/help/plugins.md @@ -67,9 +67,9 @@ which micro defines: by the user. Returns a boolean which defines whether the action should be canceled. -* `onRune(rune)`: runs when the composed rune has been inserted +* `onRune(bufpane, rune)`: runs when the composed rune has been inserted -* `preRune(rune)`: runs before the composed rune will be inserted +* `preRune(bufpane, rune)`: runs before the composed rune will be inserted For example a function which is run every time the user saves the buffer would be: @@ -367,6 +367,7 @@ strings regexp errors time +unicode/utf8 archive/zip net/http ``` diff --git a/runtime/plugins/autoclose/autoclose.lua b/runtime/plugins/autoclose/autoclose.lua index 531b7601..f1fc2fad 100644 --- a/runtime/plugins/autoclose/autoclose.lua +++ b/runtime/plugins/autoclose/autoclose.lua @@ -50,11 +50,11 @@ function preInsertNewline(bp) for i = 1, #autoNewlinePairs do if curRune == charAt(autoNewlinePairs[i], 1) then if nextRune == charAt(autoNewlinePairs[i], 2) then - bp:InsertNewline() - bp:InsertTab() bp.Buf:Insert(-bp.Cursor.Loc, "\n" .. ws) bp:StartOfLine() bp:CursorLeft() + bp:InsertNewline() + bp:InsertTab() return false end end diff --git a/runtime/syntax/c.yaml b/runtime/syntax/c.yaml index cf19b25a..4d603234 100644 --- a/runtime/syntax/c.yaml +++ b/runtime/syntax/c.yaml @@ -5,18 +5,22 @@ detect: rules: - identifier: "\\b[A-Z_][0-9A-Z_]+\\b" - - type: "\\b(float|double|bool|char|int|short|long|enum|void|struct|union|typedef|(un)?signed|inline)\\b" - - type: "\\b((s?size)|((u_?)?int(8|16|32|64|ptr)))_t\\b" + - type: "\\b(_Atomic|_BitInt|float|double|_Decimal32|_Decimal64|_Decimal128|_Complex|complex|_Imaginary|imaginary|_Bool|bool|char|int|short|long|enum|void|struct|union|typedef|typeof|typeof_unqual|(un)?signed|inline|_Noreturn)\\b" + - type: "\\b((s?size)|((u_?)?int(8|16|32|64|ptr))|char(8|16|32)|wchar)_t\\b" + # GCC float/decimal/fixed types + - type: "\\b(_Float16|__fp16|_Float32|_Float32x|_Float64|_Float64x|__float80|_Float128|_Float128x|__float128|__ibm128|__int128|_Fract|_Sat|_Accum)\\b" - type: "\\b[a-z_][0-9a-z_]+(_t|_T)\\b" - - statement: "\\b(auto|volatile|register|restrict|static|const|extern)\\b" - - statement: "\\b(for|if|while|do|else|case|default|switch)\\b" + - statement: "\\b(auto|volatile|register|restrict|_Alignas|alignas|_Alignof|alignof|static|const|constexpr|extern|_Thread_local|thread_local)\\b" + - statement: "\\b(for|if|while|do|else|case|default|switch|_Generic|_Static_assert|static_assert)\\b" - statement: "\\b(goto|continue|break|return)\\b" - - preproc: "^[[:space:]]*#[[:space:]]*(define|pragma|include|(un|ifn?)def|endif|el(if|se)|if|warning|error)" + - statement: "\\b(asm|fortran)\\b" + - preproc: "^[[:space:]]*#[[:space:]]*(define|embed|pragma|include|(un|ifn?)def|endif|el(if|ifdef|ifndef|se)|if|line|warning|error|__has_include|__has_embed|__has_c_attribute)" + - preproc: "^[[:space:]]*_Pragma\\b" # GCC builtins - statement: "__attribute__[[:space:]]*\\(\\([^)]*\\)\\)" - - statement: "__(aligned|asm|builtin|hidden|inline|packed|restrict|section|typeof|weak)__" + - statement: "__(aligned|asm|builtin|extension|hidden|inline|packed|restrict|section|typeof|weak)__" # Operator Color - - symbol.operator: "[-+*/%=<>.:;,~&|^!?]|\\b(sizeof)\\b" + - symbol.operator: "[-+*/%=<>.:;,~&|^!?]|\\b(offsetof|sizeof)\\b" - symbol.brackets: "[(){}]|\\[|\\]" # Integer Constants - constant.number: "(\\b([1-9][0-9]*|0[0-7]*|0[Xx][0-9A-Fa-f]+|0[Bb][01]+)([Uu][Ll]?[Ll]?|[Ll][Ll]?[Uu]?)?\\b)" diff --git a/runtime/syntax/cake.yaml b/runtime/syntax/cake.yaml new file mode 100644 index 00000000..555b4812 --- /dev/null +++ b/runtime/syntax/cake.yaml @@ -0,0 +1,7 @@ +filetype: cake +detect: + filename: "\\.cake$" + +rules: + - include: "csharp" + - preproc: "^[[:space:]]*#(addin|break|l|load|module|r|reference|tool)" diff --git a/runtime/syntax/csharp.yaml b/runtime/syntax/csharp.yaml index 620aada0..e78a19cf 100644 --- a/runtime/syntax/csharp.yaml +++ b/runtime/syntax/csharp.yaml @@ -10,10 +10,11 @@ rules: # Annotation - identifier.var: "@[A-Za-z]+" + - preproc: "^[[:space:]]*#[[:space:]]*(define|elif|else|endif|endregion|error|if|line|nullable|pragma|region|undef|warning)" - identifier: "([A-Za-z0-9_]*[[:space:]]*[()])" - - type: "\\b(bool|byte|sbyte|char|decimal|double|float|IntPtr|int|uint|long|ulong|object|short|ushort|string|base|this|var|void)\\b" - - statement: "\\b(alias|as|case|catch|checked|default|do|dynamic|else|finally|fixed|for|foreach|goto|if|is|lock|new|null|return|switch|throw|try|unchecked|while)\\b" - - statement: "\\b(abstract|async|class|const|delegate|enum|event|explicit|extern|get|implicit|in|internal|interface|namespace|operator|out|override|params|partial|private|protected|public|readonly|ref|sealed|set|sizeof|stackalloc|static|struct|typeof|unsafe|using|value|virtual|volatile|yield)\\b" + - type: "\\b(bool|byte|sbyte|char|decimal|double|float|IntPtr|int|uint|long|ulong|managed|unmanaged|nint|nuint|object|short|ushort|string|base|this|var|void)\\b" + - statement: "\\b(alias|as|case|catch|checked|default|do|dynamic|else|finally|fixed|for|foreach|goto|if|is|lock|new|null|return|switch|throw|try|unchecked|when|while|with)\\b" + - statement: "\\b(abstract|add|and|args|async|await|class|const|delegate|enum|event|explicit|extern|file|get|global|implicit|in|init|internal|interface|nameof|namespace|not|notnull|operator|or|out|override|params|partial|private|protected|public|readonly|record|ref|remove|required|scoped|sealed|set|sizeof|stackalloc|static|struct|typeof|unsafe|using|value|virtual|volatile|yield)\\b" # LINQ-only keywords (ones that cannot be used outside of a LINQ query - lots others can) - statement: "\\b(from|where|select|group|info|orderby|join|let|in|on|equals|by|ascending|descending)\\b" - special: "\\b(break|continue)\\b" diff --git a/runtime/syntax/gomod.yaml b/runtime/syntax/gomod.yaml new file mode 100644 index 00000000..5f2b7c68 --- /dev/null +++ b/runtime/syntax/gomod.yaml @@ -0,0 +1,31 @@ +filetype: gomod + +detect: + filename: "go.mod" + +rules: + # URL + - type: "(^|[ \\t])+\\b([a-zA-Z0-9-]+\\.?)+(/[a-zA-Z0-9-_\\.]+)*\\b" + + # Keywords + - special: "(^|[ \\t])+\\b(module|go)\\b" + - preproc: "(^|[ \\t])+\\b(toolchain|require|exclude|replace|retract)\\b" + - symbol.operator: "=>" + + # Brackets + - type: "(\\(|\\))" + + # Go version + - type: "(^|[ \\t])+([0-9]+\\.?)+" + + # Version + - constant.string: "(^|[ \\t])+v([0-9]+\\.?){3}.*" + - constant.number: "(^|[ \\t])+v([0-9]+\\.?){3}" + + - comment: + start: "//" + end: "$" + rules: + - todo: "(indirect):?" + +# (^|[ \\t])+ means after start of string or space or tab character diff --git a/runtime/syntax/html.yaml b/runtime/syntax/html.yaml index 6386c97e..70c136c8 100644 --- a/runtime/syntax/html.yaml +++ b/runtime/syntax/html.yaml @@ -8,7 +8,7 @@ rules: - preproc: "" # Opening tag - symbol.tag: - start: "<(a|abbr|acronym|address|applet|area|article|aside|audio|b|base|bdi|bdo|big|blockquote|body|br|button|canvas|caption|center|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|dir|div|dl|dt|em|embed|fieldset|figcaption|figure|font|footer|form|frame|frameset|h[1-6]|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|menu|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|s|samp|section|select|small|source|span|strike|strong|sub|summary|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|track|tt|u|ul|var|video|wbr)\\b" + start: "<(a|abbr|acronym|address|applet|area|article|aside|audio|b|base|bdi|bdo|big|blockquote|body|br|button|canvas|caption|center|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|dir|div|dl|dt|em|embed|fieldset|figcaption|figure|font|footer|form|frame|frameset|h[1-6]|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|main|mark|menu|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|s|samp|section|select|small|source|span|strike|strong|sub|summary|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|track|tt|u|ul|var|video|wbr)\\b" end: ">" rules: - identifier: "\\b(placeholder|style|alt|bgcolor|height|href|id|(aria|data)\\-.+|label|longdesc|name|on(click|focus|load|mouseover)|size|span|src|target|type|value|width|class|charset|content|rel|integrity|crossorigin|for|onsubmit|lang|role)\\b" @@ -34,7 +34,7 @@ rules: # Closing tag - symbol.tag: - start: "" rules: # Anything in the closing tag is an error diff --git a/runtime/syntax/kvlang.yaml b/runtime/syntax/kvlang.yaml new file mode 100644 index 00000000..e40d348f --- /dev/null +++ b/runtime/syntax/kvlang.yaml @@ -0,0 +1,67 @@ +filetype: "kvlang" + +detect: + filename: "\\.kv$" + +rules: +# layouts +- special: "\\b[a-z].+" +- identifier: "\\b(self|app|root)\\b" + +- type: "\\b[A-Z].+" +- type: "\\b(AnchorLayout|BoxLayout|FloatLayout|RelativeLayout|GridLayout|PageLayout|StackLayout)\\b" + +- type: "\\b(canvas)\\b" + +# functions +- identifier.function: "[a-zA-Z_0-9]+\\(" + +# built-in functions +- type: "\\b(abs|all|any|ascii|bin|bool|breakpoint|bytearray|bytes)\\b" +- type: "\\b(callable|chr|classmethod|compile|copyright|credits|oct)\\b" +- type: "\\b(delattr|dict|dir|display|divmod|enumerate|eval|filter)\\b" +- type: "\\b(float|format|frozenset|get_ipython|getattr|globals|type)\\b" +- type: "\\b(hash|help|hex|id|input|int|isinstance|issubclass|iter|len)\\b" +- type: "\\b(license|list|locals|map|max|memoryview|min|next|object)\\b" +- type: "\\b(open|ord|pow|print|property|range|repr|reversed|round|set)\\b" +- type: "\\b(setattr|slice|sorted|staticmethod|hasattr|super|tuple|str)\\b" +- type: "\\b(vars|zip|exec|sum|complex)\\b" + +# keywords +- statement.built_in: "\\b(and|as|assert|async|await|break|class|continue|def)\\b" +- statement.built_in: "\\b(del|elif|else|except|finally|for|from|global|if)\\b" +- statement.built_in: "\\b(import|in|is|lambda|nonlocal|not|or|pass|raise)\\b" +- statement.built_in: "\\b(return|try|while|with|yield|match|case)\\b" + +# operators +- symbol.operator: "([~^.:;,+*|=!\\%]|<|>|/|-|&)" + +# parentheses +- symbol.brackets: "([(){}]|\\[|\\])" + +# numbers +- constant.number: "\\b[0-9](_?[0-9])*(\\.([0-9](_?[0-9])*)?)?(e[0-9](_?[0-9])*)?\\b" # decimal +- constant.number: "\\b0b(_?[01])+\\b" # bin +- constant.number: "\\b0o(_?[0-7])+\\b" # oct +- constant.number: "\\b0x(_?[0-9a-f])+\\b" # hex + +- constant.bool.none: "\\b(None)\\b" +- constant.bool.true: "\\b(True)\\b" +- constant.bool.false: "\\b(False)\\b" + +# strings +- constant.string: + start: "\"" + end: "(\"|$)" + skip: "\\\\." + rules: [] +- constant.string: + start: "'" + end: "('|$)" + skip: "\\\\." + rules: [] + +- comment: + start: "#" + end: "$" + rules: [] diff --git a/runtime/syntax/log.yaml b/runtime/syntax/log.yaml new file mode 100644 index 00000000..fa01238f --- /dev/null +++ b/runtime/syntax/log.yaml @@ -0,0 +1,94 @@ +filetype: log + +detect: + filename: "(\\.log|log\\.txt)$" + +rules: +- diff-modified: "\\b(WARN(ING)?|[Ww]arn(ing)?|w(r)?n|w|W/)\\b" +- diff-modified: "\\b(CRITICAL|[Cc]ritical)\\b" + +- constant: "\\b(INFO(RMATION)?|[Ii]nfo(rmation)?|[Ii]n(f)?|i|I/)\\b" +- constant: "\\b(DEBUG|[Dd]ebug|dbug|dbg|de|d|D/)\\b" +- constant: "\\b(VERBOSE|[Vv]erbose|V/)\\b" +- constant: "\\b(ALERT|[Aa]lert)\\b" + +- preproc: "\\b(TRACE|Trace|NOTICE|VERBOSE|verb|vrb|vb|v)\\b" + +- gutter-error: "\\b(ERROR|[Ee]rr(or)?|[Ee]r(or)?|e|E\\x2F)\\b" +- gutter-error: "\\b(FATAL|[Ff]atal)\\b" +- gutter-error: "\\b(EMERGENCY|[Ee]mergency)\\b" +- gutter-error: "\\b(FAIL(URE)?|[Ff]ail(ure)?)\\b" + +# constants +- constant.bool.true: "\\b(YES|yes|Y|y|ON|on|TRUE|True|true)\\b" +- constant.bool.false: "\\b(NO|no|N|n|OFF|off|FALSE|False|false)\\b" +- constant.bool.false: "\\b(None|null|nil)\\b" + +# numbers +- constant.number: "\\b[0-9](_?[0-9])*(\\.([0-9](_?[0-9])*)?)?(e[0-9](_?[0-9])*)?\\b" # decimal +- constant.number: "\\b0b(_?[01])+\\b" # bin +- constant.number: "\\b0o(_?[0-7])+\\b" # oct +- constant.number: "\\b0x(_?[0-9a-f])+\\b" # hex + +# operators +- symbol.operator: "([~^.:;,+*|=!\\%]|<|>|/|-|&)" + +# parentheses +- symbol.brackets: "([(){}]|\\[|\\])" + +# string +- constant.string: + start: "\"" + end: "(\"|$)" + skip: "\\\\." + rules: + - constant.specialChar: "\\\\." + +- constant.string: + start: "'" + end: "('|$)" + skip: "\\\\." + rules: + - constant.specialChar: "\\\\." + +# file +- preproc: "\\b(FILE|File|file)\\b" + +# time +- identifier: "\\b((([Mm]on|[Tt]ues|[Ww]ed(nes)?|[Tt]hur(s)?|[Ff]ri|[Ss]at(ur)?|[Ss]un)(day)?\\s)?([Jj]an(uary)?|[Ff]eb(ruary)?|[Mm]ar(ch)?|[Aa]pr(il)?|[Mm]ay|[Jj]un(e)?|[Jj]ul(y)?|[Aa]ug(ust)?|[Aa]go|[Ss]ep(tember)?|[Oo]ct(ober)?|[Nn]ov(ember)?|[Dd]ec(ember)?)\\s\\d{1,2},?(\\s\\d{4})?)\\b" # date +- identifier: "\\b(\\d{2,4}[-/\\.]?\\d{2,3}[-/\\.]?\\d{2,4})\\b" # date +- identifier: "\\b(\\d{2}:\\d{2}(:\\d{2})?([\\.,]?\\d{1,8}[\\.\\+,]?\\d{1,8}?)?([\\.\\+,]?\\d{1,8}[\\.\\+,]?\\d{1,8}?)?([\\.\\+,]?\\d{1,8}?)?(\\s-\\d{0,4})?)\\b" # time +- identifier: "^([0-2][0-9][0-2][0-9][-/]?[0-9][0-9][-/]?[0-9][0-9])" +# - identifier: "^([0-2][0-9][0-2][0-9][-/]?[0-9][0-9][-/]?[0-9][0-9]\\s[0-9][0-9]:[0-9][0-9](:[0-9][0-9])?(\\.?[0-9][0-9][0-9])?)" +- identifier: "^(\\d{4}[-/]?\\d{2}[-/]?\\d{2}\\s\\d{2}:\\d{2}(:\\d{2})?(\\.?\\d{2,8})?)" +- identifier: "^([0-2][0-9]|[0-2]-?[0-9][0-9]-?[0-9][0-9])\\-([0-1][0-9])\\-([0-3][0-9]) ([0-2][0-9])\\:([0-5][0-9])\\:([0-5][0-9]),([0-9][0-9][0-9])" +# ISO 8601:2004(E) +- identifier: "" +# Complete precision: +- identifier: "^(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d:[0-5]\\d|Z))" +# No milliseconds: +- identifier: "^(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d([+-][0-2]\\d:[0-5]\\d|Z))" +# No Seconds: +- identifier: "^(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d([+-][0-2]\\d:[0-5]\\d|Z))" +# Putting it all together: +- identifier: "^(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d:[0-5]\\d|Z))|(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d([+-][0-2]\\d:[0-5]\\d|Z))|(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d([+-][0-2]\\d:[0-5]\\d|Z))" +# Complete precision: +- identifier: "^(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+)" +# No milliseconds +- identifier: "^(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d)" +# No Seconds +- identifier: "^(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d)" +# Putting it all together +- identifier: "^(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+)|(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d)|(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d)" + +# link +- constant.string.url: + start: "https?://" + end: "\\s" + rules: [] + +# path +# - constant.string.url: "\\b(.+)/([^/]+)\\b" # linux +# - constant.string.url: "\\b(^[a-zA-Z]:)\\b" # windowns + +- diff-modified: "([Cc]ommit:)\\s\\w+\\[\\w+]" diff --git a/runtime/syntax/msbuild.yaml b/runtime/syntax/msbuild.yaml new file mode 100644 index 00000000..2c792137 --- /dev/null +++ b/runtime/syntax/msbuild.yaml @@ -0,0 +1,6 @@ +filetype: msbuild +detect: + filename: "\\.(.*proj|props|targets|tasks)$" + +rules: + - include: "xml" diff --git a/runtime/syntax/sh.yaml b/runtime/syntax/sh.yaml index 6c122de2..38c9c230 100644 --- a/runtime/syntax/sh.yaml +++ b/runtime/syntax/sh.yaml @@ -39,7 +39,7 @@ rules: # Coreutils commands - type: "\\b(base64|basename|cat|chcon|chgrp|chmod|chown|chroot|cksum|comm|cp|csplit|cut|date|dd|df|dir|dircolors|dirname|du|env|expand|expr|factor|false|fmt|fold|head|hostid|id|install|join|link|ln|logname|ls|md5sum|mkdir|mkfifo|mknod|mktemp|mv|nice|nl|nohup|nproc|numfmt|od|paste|pathchk|pinky|pr|printenv|printf|ptx|pwd|readlink|realpath|rm|rmdir|runcon|seq|(sha1|sha224|sha256|sha384|sha512)sum|shred|shuf|sleep|sort|split|stat|stdbuf|stty|sum|sync|tac|tail|tee|test|time|timeout|touch|tr|true|truncate|tsort|tty|uname|unexpand|uniq|unlink|users|vdir|wc|who|whoami|yes)\\b" # Conditional flags - - statement: " (-[A-Za-z]+|--[a-z]+)" + - statement: "\\s+(-[A-Za-z]+|--[a-z]+)" - identifier: "\\$\\{[0-9A-Za-z_:!%&=+#~@*^$?, .\\-\\/\\[\\]]+\\}" - identifier: "\\$[0-9A-Za-z_:!%&=+#~@*^$?,\\-\\[\\]]+"