Compare commits
No commits in common. "93968e7333adf525dbd6952f1fc639806b4e3dc0" and "ccb061989a80b3f16ba8a5f89e5ad61b90562498" have entirely different histories.
93968e7333
...
ccb061989a
@ -16,7 +16,6 @@ func main() {
|
|||||||
|
|
||||||
win := core.NewWindowBuilder().
|
win := core.NewWindowBuilder().
|
||||||
WithBuffer(&buf).
|
WithBuffer(&buf).
|
||||||
WithOptions(core.NewDefaultWinOptions()).
|
|
||||||
Build()
|
Build()
|
||||||
|
|
||||||
model := editor.NewModelBuilder().
|
model := editor.NewModelBuilder().
|
||||||
|
|||||||
@ -241,7 +241,7 @@ func (a InsertTab) Execute(m Model) tea.Cmd {
|
|||||||
|
|
||||||
x, y := win.Cursor.Col, win.Cursor.Line
|
x, y := win.Cursor.Col, win.Cursor.Line
|
||||||
l := buf.Lines[y]
|
l := buf.Lines[y]
|
||||||
tabs := strings.Repeat(" ", m.Settings().TabStop)
|
tabs := strings.Repeat(" ", m.Settings().TabSize)
|
||||||
if x < len(l) {
|
if x < len(l) {
|
||||||
buf.SetLine(y, l[:x]+tabs+l[x:])
|
buf.SetLine(y, l[:x]+tabs+l[x:])
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -45,8 +45,8 @@ type Model interface {
|
|||||||
Mode() core.Mode
|
Mode() core.Mode
|
||||||
SetMode(mode core.Mode)
|
SetMode(mode core.Mode)
|
||||||
|
|
||||||
Settings() core.EditorSettings
|
Settings() core.Settings
|
||||||
SetSettings(s core.EditorSettings)
|
SetSettings(s core.Settings)
|
||||||
|
|
||||||
// ==================================================
|
// ==================================================
|
||||||
// Registers
|
// Registers
|
||||||
@ -55,6 +55,7 @@ type Model interface {
|
|||||||
GetRegister(name rune) (core.Register, bool)
|
GetRegister(name rune) (core.Register, bool)
|
||||||
SetRegister(name rune, t core.RegisterType, cnt []string) error
|
SetRegister(name rune, t core.RegisterType, cnt []string) error
|
||||||
UpdateDefaultRegister(t core.RegisterType, cnt []string)
|
UpdateDefaultRegister(t core.RegisterType, cnt []string)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action is the base interface - anything executable
|
// Action is the base interface - anything executable
|
||||||
|
|||||||
@ -110,15 +110,7 @@ type Setting struct {
|
|||||||
Name string
|
Name string
|
||||||
ShortForm string
|
ShortForm string
|
||||||
Type SettingType
|
Type SettingType
|
||||||
Get func(s core.EditorSettings) any
|
Get func(s core.Settings) any
|
||||||
Set func(m action.Model, val any)
|
|
||||||
}
|
|
||||||
|
|
||||||
type WindowSetting struct {
|
|
||||||
Name string
|
|
||||||
ShortForm string
|
|
||||||
Type SettingType
|
|
||||||
Get func(m action.Model) any
|
|
||||||
Set func(m action.Model, val any)
|
Set func(m action.Model, val any)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,69 +123,50 @@ const (
|
|||||||
StringSetting
|
StringSetting
|
||||||
)
|
)
|
||||||
|
|
||||||
// settingsMap defines all available editor settings
|
// settingsMap defines all available settings
|
||||||
var settingsMap = []Setting{
|
var settingsMap = []Setting{
|
||||||
{
|
|
||||||
Name: "tabstop",
|
|
||||||
ShortForm: "ts",
|
|
||||||
Type: IntSetting,
|
|
||||||
Get: func(s core.EditorSettings) any { return s.TabStop },
|
|
||||||
Set: func(m action.Model, val any) {
|
|
||||||
s := m.Settings()
|
|
||||||
s.TabStop = val.(int)
|
|
||||||
m.SetSettings(s)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// windowSettingsMap defines all available window settings
|
|
||||||
var windowSettingsMap = []WindowSetting{
|
|
||||||
{
|
{
|
||||||
Name: "number",
|
Name: "number",
|
||||||
ShortForm: "nu",
|
ShortForm: "nu",
|
||||||
Type: BoolSetting,
|
Type: BoolSetting,
|
||||||
Get: func(m action.Model) any { return m.ActiveWindow().Options.Number },
|
Get: func(s core.Settings) any { return s.Number },
|
||||||
Set: func(m action.Model, val any) {
|
Set: func(m action.Model, val any) {
|
||||||
w := m.ActiveWindow()
|
s := m.Settings()
|
||||||
o := w.Options
|
s.Number = val.(bool)
|
||||||
o.Number = val.(bool)
|
m.SetSettings(s)
|
||||||
w.SetOptions(o)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "relativenumber",
|
Name: "relativenumber",
|
||||||
ShortForm: "rnu",
|
ShortForm: "rnu",
|
||||||
Type: BoolSetting,
|
Type: BoolSetting,
|
||||||
Get: func(m action.Model) any { return m.ActiveWindow().Options.RelativeNumber },
|
Get: func(s core.Settings) any { return s.RelativeNumber },
|
||||||
Set: func(m action.Model, val any) {
|
Set: func(m action.Model, val any) {
|
||||||
w := m.ActiveWindow()
|
s := m.Settings()
|
||||||
o := w.Options
|
s.RelativeNumber = val.(bool)
|
||||||
o.RelativeNumber = val.(bool)
|
m.SetSettings(s)
|
||||||
w.SetOptions(o)
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tabstop",
|
||||||
|
ShortForm: "ts",
|
||||||
|
Type: IntSetting,
|
||||||
|
Get: func(s core.Settings) any { return s.TabSize },
|
||||||
|
Set: func(m action.Model, val any) {
|
||||||
|
s := m.Settings()
|
||||||
|
s.TabSize = val.(int)
|
||||||
|
m.SetSettings(s)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "scrolloff",
|
Name: "scrolloff",
|
||||||
ShortForm: "so",
|
ShortForm: "so",
|
||||||
Type: IntSetting,
|
Type: IntSetting,
|
||||||
Get: func(m action.Model) any { return m.ActiveWindow().Options.ScrollOff },
|
Get: func(s core.Settings) any { return s.ScrollOff },
|
||||||
Set: func(m action.Model, val any) {
|
Set: func(m action.Model, val any) {
|
||||||
w := m.ActiveWindow()
|
s := m.Settings()
|
||||||
o := w.Options
|
s.ScrollOff = val.(int)
|
||||||
o.ScrollOff = val.(int)
|
m.SetSettings(s)
|
||||||
w.SetOptions(o)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "guttersize",
|
|
||||||
ShortForm: "gu",
|
|
||||||
Type: IntSetting,
|
|
||||||
Get: func(m action.Model) any { return m.ActiveWindow().Options.GutterSize },
|
|
||||||
Set: func(m action.Model, val any) {
|
|
||||||
w := m.ActiveWindow()
|
|
||||||
o := w.Options
|
|
||||||
o.GutterSize = val.(int)
|
|
||||||
w.SetOptions(o)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -213,27 +186,14 @@ func lookupSetting(name string) *Setting {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookupWindowSetting: Finds a window setting by name, short form, or prefix.
|
|
||||||
func lookupWindowSetting(name string) *WindowSetting {
|
|
||||||
for i := range windowSettingsMap {
|
|
||||||
s := &windowSettingsMap[i]
|
|
||||||
if name == s.Name || name == s.ShortForm {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
// Prefix matching
|
|
||||||
if len(name) >= len(s.ShortForm) && strings.HasPrefix(s.Name, name) {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseSetOption: Parses and applies a single :set option.
|
// parseSetOption: Parses and applies a single :set option.
|
||||||
func parseSetOption(m action.Model, opt string) error {
|
func parseSetOption(m action.Model, opt string) error {
|
||||||
// Handle toggle: option!
|
// Handle toggle: option!
|
||||||
if name, ok := strings.CutSuffix(opt, "!"); ok {
|
if name, ok := strings.CutSuffix(opt, "!"); ok {
|
||||||
setting := lookupSetting(name)
|
setting := lookupSetting(name)
|
||||||
if setting != nil {
|
if setting == nil {
|
||||||
|
return nil // Unknown setting
|
||||||
|
}
|
||||||
if setting.Type == BoolSetting {
|
if setting.Type == BoolSetting {
|
||||||
// Toggle the boolean
|
// Toggle the boolean
|
||||||
currentVal := setting.Get(m.Settings()).(bool)
|
currentVal := setting.Get(m.Settings()).(bool)
|
||||||
@ -242,47 +202,26 @@ func parseSetOption(m action.Model, opt string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
windowSetting := lookupWindowSetting(name)
|
|
||||||
if windowSetting != nil {
|
|
||||||
if windowSetting.Type == BoolSetting {
|
|
||||||
// Toggle the boolean
|
|
||||||
currentVal := windowSetting.Get(m).(bool)
|
|
||||||
windowSetting.Set(m, !currentVal)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle disable: nooption
|
// Handle disable: nooption
|
||||||
if name, ok := strings.CutPrefix(opt, "no"); ok {
|
if name, ok := strings.CutPrefix(opt, "no"); ok {
|
||||||
setting := lookupSetting(name)
|
setting := lookupSetting(name)
|
||||||
if setting != nil {
|
if setting == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if setting.Type == BoolSetting {
|
if setting.Type == BoolSetting {
|
||||||
setting.Set(m, false)
|
setting.Set(m, false)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
windowSetting := lookupWindowSetting(name)
|
|
||||||
if windowSetting != nil {
|
|
||||||
if windowSetting.Type == BoolSetting {
|
|
||||||
windowSetting.Set(m, false)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle assignment: option=value
|
// Handle assignment: option=value
|
||||||
if strings.Contains(opt, "=") {
|
if strings.Contains(opt, "=") {
|
||||||
parts := strings.SplitN(opt, "=", 2)
|
parts := strings.SplitN(opt, "=", 2)
|
||||||
name, value := parts[0], parts[1]
|
name, value := parts[0], parts[1]
|
||||||
|
|
||||||
setting := lookupSetting(name)
|
setting := lookupSetting(name)
|
||||||
if setting != nil {
|
if setting == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
switch setting.Type {
|
switch setting.Type {
|
||||||
case IntSetting:
|
case IntSetting:
|
||||||
intVal, err := strconv.Atoi(value)
|
intVal, err := strconv.Atoi(value)
|
||||||
@ -300,44 +239,14 @@ func parseSetOption(m action.Model, opt string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
windowSetting := lookupWindowSetting(name)
|
|
||||||
if windowSetting != nil {
|
|
||||||
switch windowSetting.Type {
|
|
||||||
case IntSetting:
|
|
||||||
intVal, err := strconv.Atoi(value)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
windowSetting.Set(m, intVal)
|
|
||||||
case StringSetting:
|
|
||||||
windowSetting.Set(m, value)
|
|
||||||
case BoolSetting:
|
|
||||||
// Handle :set option=true / :set option=false
|
|
||||||
boolVal := value == "true" || value == "1" || value == "yes"
|
|
||||||
windowSetting.Set(m, boolVal)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle enable: option (boolean only)
|
// Handle enable: option (boolean only)
|
||||||
setting := lookupSetting(opt)
|
setting := lookupSetting(opt)
|
||||||
if setting != nil {
|
if setting == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if setting.Type == BoolSetting {
|
if setting.Type == BoolSetting {
|
||||||
setting.Set(m, true)
|
setting.Set(m, true)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
windowSetting := lookupWindowSetting(opt)
|
|
||||||
if windowSetting != nil {
|
|
||||||
if windowSetting.Type == BoolSetting {
|
|
||||||
windowSetting.Set(m, true)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,23 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
// EditorSettings: Configuration options for editor display and behavior.
|
// Settings: Configuration options for editor display and behavior.
|
||||||
type EditorSettings struct {
|
type Settings struct {
|
||||||
TabStop int
|
Number bool
|
||||||
|
RelativeNumber bool
|
||||||
|
GutterSize int
|
||||||
|
TabSize int
|
||||||
|
ScrollOff int
|
||||||
// TODO: Colors
|
// TODO: Colors
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDefaultSettings: Creates a Settings struct with sensible defaults for
|
// NewDefaultSettings: Creates a Settings struct with sensible defaults for
|
||||||
// line numbers, gutter width, tab size, and scroll offset.
|
// line numbers, gutter width, tab size, and scroll offset.
|
||||||
func NewDefaultSettings() EditorSettings {
|
func NewDefaultSettings() Settings {
|
||||||
return EditorSettings{
|
return Settings{
|
||||||
TabStop: 2,
|
Number: true,
|
||||||
|
RelativeNumber: true,
|
||||||
|
GutterSize: 5,
|
||||||
|
TabSize: 2,
|
||||||
|
ScrollOff: 8,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,22 +2,12 @@ package core
|
|||||||
|
|
||||||
// TODO: No more global settings, window-wide settings
|
// TODO: No more global settings, window-wide settings
|
||||||
type WinOptions struct {
|
type WinOptions struct {
|
||||||
Number bool
|
// Number bool
|
||||||
RelativeNumber bool
|
|
||||||
GutterSize int
|
|
||||||
// Wrap bool
|
// Wrap bool
|
||||||
|
// Relnumber bool
|
||||||
ScrollOff int
|
ScrollOff int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultWinOptions() WinOptions {
|
|
||||||
return WinOptions{
|
|
||||||
Number: true,
|
|
||||||
RelativeNumber: true,
|
|
||||||
GutterSize: 5,
|
|
||||||
ScrollOff: 8,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Window struct {
|
type Window struct {
|
||||||
Id int
|
Id int
|
||||||
Number int // Ignored for now, will be used when splits come into play
|
Number int // Ignored for now, will be used when splits come into play
|
||||||
@ -72,10 +62,8 @@ func (w *Window) AdjustScroll() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
viewPort := w.ViewportHeight()
|
|
||||||
|
|
||||||
// Effective scrollOff (can't be more than half the viewport)
|
// Effective scrollOff (can't be more than half the viewport)
|
||||||
off := min(w.Options.ScrollOff, viewPort/2)
|
off := min(w.Options.ScrollOff, w.Height/2)
|
||||||
|
|
||||||
// Cursor too close to top — scroll up
|
// Cursor too close to top — scroll up
|
||||||
if w.Cursor.Line < w.ScrollY+off {
|
if w.Cursor.Line < w.ScrollY+off {
|
||||||
@ -83,27 +71,15 @@ func (w *Window) AdjustScroll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cursor too close to bottom — scroll down
|
// Cursor too close to bottom — scroll down
|
||||||
if w.Cursor.Line > w.ScrollY+viewPort-1-off {
|
if w.Cursor.Line > w.ScrollY+w.Height-1-off {
|
||||||
w.ScrollY = w.Cursor.Line - viewPort + 1 + off
|
w.ScrollY = w.Cursor.Line - w.Height + 1 + off
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp scrollY to valid range
|
// Clamp scrollY to valid range
|
||||||
maxScroll := max(0, w.Buffer.LineCount()-viewPort)
|
maxScroll := max(0, w.Buffer.LineCount()-w.Height)
|
||||||
w.ScrollY = max(0, min(w.ScrollY, maxScroll))
|
w.ScrollY = max(0, min(w.ScrollY, maxScroll))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// Getters (for computed values)
|
|
||||||
// ==================================================
|
|
||||||
|
|
||||||
func (w *Window) ViewportHeight() int {
|
|
||||||
// TODO: This will need more magic when splits come into play
|
|
||||||
|
|
||||||
// -1 for command bar
|
|
||||||
// -1 for status line
|
|
||||||
return w.Height - 2
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================================================
|
// ==================================================
|
||||||
// Setters
|
// Setters
|
||||||
// ==================================================
|
// ==================================================
|
||||||
@ -191,8 +167,3 @@ func (w *Window) SetDimensions(width, height int) {
|
|||||||
w.Width = width
|
w.Width = width
|
||||||
w.Height = height
|
w.Height = height
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window.SetOptions: Sets the options of this window.
|
|
||||||
func (w *Window) SetOptions(opts WinOptions) {
|
|
||||||
w.Options = opts
|
|
||||||
}
|
|
||||||
|
|||||||
@ -20,7 +20,9 @@ func NewWindowBuilder() *WindowBuilder {
|
|||||||
ScrollY: 0,
|
ScrollY: 0,
|
||||||
Height: 0,
|
Height: 0,
|
||||||
Width: 0,
|
Width: 0,
|
||||||
Options: NewDefaultWinOptions(),
|
Options: WinOptions{
|
||||||
|
ScrollOff: 8, // 8 is default
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ func TestCommandSetBoolean(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "n", "o", "n", "u", "m", "b", "e", "r", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "n", "o", "n", "u", "m", "b", "e", "r", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.ActiveWindow().Options.Number {
|
if m.Settings().Number {
|
||||||
t.Error("expected Number to be false after :set nonumber")
|
t.Error("expected Number to be false after :set nonumber")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -33,7 +33,7 @@ func TestCommandSetBoolean(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "n", "u", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "n", "u", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if !m.ActiveWindow().Options.Number {
|
if !m.Settings().Number {
|
||||||
t.Error("expected Number to be true after :set nu")
|
t.Error("expected Number to be true after :set nu")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -46,7 +46,7 @@ func TestCommandSetBoolean(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "n", "u", "m", "b", "e", "r", "!", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "n", "u", "m", "b", "e", "r", "!", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.ActiveWindow().Options.Number {
|
if m.Settings().Number {
|
||||||
t.Error("expected Number to be false after :set number!")
|
t.Error("expected Number to be false after :set number!")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -60,7 +60,7 @@ func TestCommandSetBoolean(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "n", "u", "!", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "n", "u", "!", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if !m.ActiveWindow().Options.Number {
|
if !m.Settings().Number {
|
||||||
t.Error("expected Number to be true after double toggle")
|
t.Error("expected Number to be true after double toggle")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -73,7 +73,7 @@ func TestCommandSetBoolean(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "n", "o", "r", "n", "u", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "n", "o", "r", "n", "u", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.ActiveWindow().Options.RelativeNumber {
|
if m.Settings().RelativeNumber {
|
||||||
t.Error("expected RelativeNumber to be false after :set nornu")
|
t.Error("expected RelativeNumber to be false after :set nornu")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -87,7 +87,7 @@ func TestCommandSetBoolean(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "r", "n", "u", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "r", "n", "u", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if !m.ActiveWindow().Options.RelativeNumber {
|
if !m.Settings().RelativeNumber {
|
||||||
t.Error("expected RelativeNumber to be true after :set rnu")
|
t.Error("expected RelativeNumber to be true after :set rnu")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -95,15 +95,15 @@ func TestCommandSetBoolean(t *testing.T) {
|
|||||||
|
|
||||||
func TestCommandSetInteger(t *testing.T) {
|
func TestCommandSetInteger(t *testing.T) {
|
||||||
t.Run("':set tabstop=4' sets tab size", func(t *testing.T) {
|
t.Run("':set tabstop=4' sets tab size", func(t *testing.T) {
|
||||||
// Default TabStop=2
|
// Default TabSize=2
|
||||||
lines := []string{"hello"}
|
lines := []string{"hello"}
|
||||||
tm := newTestModelWithLines(t, lines)
|
tm := newTestModelWithLines(t, lines)
|
||||||
|
|
||||||
sendKeys(tm, ":", "s", "e", "t", " ", "t", "a", "b", "s", "t", "o", "p", "=", "4", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "t", "a", "b", "s", "t", "o", "p", "=", "4", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Settings().TabStop != 4 {
|
if m.Settings().TabSize != 4 {
|
||||||
t.Errorf("TabStop = %d, want 4", m.Settings().TabStop)
|
t.Errorf("TabSize = %d, want 4", m.Settings().TabSize)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -114,8 +114,8 @@ func TestCommandSetInteger(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "t", "s", "=", "8", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "t", "s", "=", "8", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.Settings().TabStop != 8 {
|
if m.Settings().TabSize != 8 {
|
||||||
t.Errorf("TabStop = %d, want 8", m.Settings().TabStop)
|
t.Errorf("TabSize = %d, want 8", m.Settings().TabSize)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -127,8 +127,8 @@ func TestCommandSetInteger(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "s", "c", "r", "o", "l", "l", "o", "f", "f", "=", "5", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "s", "c", "r", "o", "l", "l", "o", "f", "f", "=", "5", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.ActiveWindow().Options.ScrollOff != 5 {
|
if m.Settings().ScrollOff != 5 {
|
||||||
t.Errorf("ScrollOff = %d, want 5", m.ActiveWindow().Options.ScrollOff)
|
t.Errorf("ScrollOff = %d, want 5", m.Settings().ScrollOff)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -139,8 +139,8 @@ func TestCommandSetInteger(t *testing.T) {
|
|||||||
sendKeys(tm, ":", "s", "e", "t", " ", "s", "o", "=", "1", "0", "enter")
|
sendKeys(tm, ":", "s", "e", "t", " ", "s", "o", "=", "1", "0", "enter")
|
||||||
|
|
||||||
m := getFinalModel(t, tm)
|
m := getFinalModel(t, tm)
|
||||||
if m.ActiveWindow().Options.ScrollOff != 10 {
|
if m.Settings().ScrollOff != 10 {
|
||||||
t.Errorf("ScrollOff = %d, want 10", m.ActiveWindow().Options.ScrollOff)
|
t.Errorf("ScrollOff = %d, want 10", m.Settings().ScrollOff)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,7 +42,7 @@ type Model struct {
|
|||||||
commandOutput string
|
commandOutput string
|
||||||
|
|
||||||
// Global settings (TODO: This needs to be refactored)
|
// Global settings (TODO: This needs to be refactored)
|
||||||
settings core.EditorSettings
|
settings core.Settings
|
||||||
|
|
||||||
// Registers
|
// Registers
|
||||||
registers map[rune]core.Register // name -> register
|
registers map[rune]core.Register // name -> register
|
||||||
@ -185,7 +185,7 @@ func (m *Model) processInsertKey(key string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "tab":
|
case "tab":
|
||||||
tabs := strings.Repeat(" ", m.Settings().TabStop)
|
tabs := strings.Repeat(" ", m.Settings().TabSize)
|
||||||
if col < len(l) {
|
if col < len(l) {
|
||||||
buf.SetLine(line, l[:col]+tabs+l[col:])
|
buf.SetLine(line, l[:col]+tabs+l[col:])
|
||||||
} else {
|
} else {
|
||||||
@ -282,11 +282,11 @@ func (m *Model) SetMode(mode core.Mode) {
|
|||||||
m.mode = mode
|
m.mode = mode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) Settings() core.EditorSettings {
|
func (m *Model) Settings() core.Settings {
|
||||||
return m.settings
|
return m.settings
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) SetSettings(s core.EditorSettings) {
|
func (m *Model) SetSettings(s core.Settings) {
|
||||||
m.settings = s
|
m.settings = s
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,3 +334,146 @@ func (m *Model) UpdateDefaultRegister(t core.RegisterType, cnt []string) {
|
|||||||
m.SetRegister('0', t, cnt)
|
m.SetRegister('0', t, cnt)
|
||||||
m.SetRegister('"', t, cnt)
|
m.SetRegister('"', t, cnt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// Depreciated
|
||||||
|
// ==================================================
|
||||||
|
// func (m *Model) Lines() []string {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// return win.Buffer.Lines
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) Line(idx int) string {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// return win.Buffer.Line(idx)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) SetLine(idx int, content string) {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// win.Buffer.SetLine(idx, content)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) InsertLine(idx int, content string) {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// win.Buffer.InsertLine(idx, content)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) DeleteLine(idx int) {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// win.Buffer.DeleteLine(idx)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) LineCount() int {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// return win.Buffer.LineCount()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) CursorX() int {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// return win.Cursor.Col
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) CursorY() int {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// return win.Cursor.Line
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) SetCursorX(x int) {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// win.Cursor.Col = x
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) SetCursorY(y int) {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// win.Cursor.Line = y
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Anchor methods
|
||||||
|
// func (m *Model) AnchorX() int {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// return win.Anchor.Col
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) AnchorY() int {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// return win.Anchor.Line
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) SetAnchorX(x int) {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// win.Anchor.Col = x
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) SetAnchorY(y int) {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// win.Anchor.Line = y
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) GetCursorPosition() *action.Position {
|
||||||
|
// // Return a copy of the position
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// pos := win.Cursor
|
||||||
|
// return &pos
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Window
|
||||||
|
// func (m *Model) ScrollY() int {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// return win.ScrollY
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) SetScrollY(y int) {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// win.ScrollY = y
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) WinH() int {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// return win.Height
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) WinW() int {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// return win.Width
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) ViewPortH() int {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// return win.Height - 2
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) ClampCursorX() {
|
||||||
|
// win := m.ActiveWindow()
|
||||||
|
// win.ClampCursorX()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m *Model) ActiveWindowId() int {
|
||||||
|
// return m.activeWindowId
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // TODO: MOVE THIS
|
||||||
|
// // AdjustScroll ensures the cursor stays within the viewport with scrollOff margins.
|
||||||
|
// // Call this after any cursor movement.
|
||||||
|
// func (m *Model) AdjustScroll() {
|
||||||
|
// viewportHeight := m.ViewPortH()
|
||||||
|
// if viewportHeight <= 0 {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Effective scrollOff (can't be more than half the viewport)
|
||||||
|
// off := min(m.Settings().ScrollOff, viewportHeight/2)
|
||||||
|
//
|
||||||
|
// // Cursor too close to top — scroll up
|
||||||
|
// if m.CursorY() < m.ScrollY()+off {
|
||||||
|
// m.SetScrollY(m.CursorY() - off)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Cursor too close to bottom — scroll down
|
||||||
|
// if m.CursorY() > m.ScrollY()+viewportHeight-1-off {
|
||||||
|
// m.SetScrollY(m.CursorY() - viewportHeight + 1 + off)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Clamp scrollY to valid range
|
||||||
|
// maxScroll := max(0, m.LineCount()-viewportHeight)
|
||||||
|
// m.SetScrollY(max(0, min(m.ScrollY(), maxScroll)))
|
||||||
|
// }
|
||||||
|
|||||||
@ -93,7 +93,7 @@ func (mb *ModelBuilder) WithTermHeight(height int) *ModelBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ModelBuilder.WithSettings: Set the editor settings (tabstop, scrolloff, etc).
|
// ModelBuilder.WithSettings: Set the editor settings (tabstop, scrolloff, etc).
|
||||||
func (mb *ModelBuilder) WithSettings(settings core.EditorSettings) *ModelBuilder {
|
func (mb *ModelBuilder) WithSettings(settings core.Settings) *ModelBuilder {
|
||||||
mb.model.settings = settings
|
mb.model.settings = settings
|
||||||
return mb
|
return mb
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,275 +5,25 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/style"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Model.View: Renders the complete editor view including buffer content, line
|
|
||||||
// numbers, status bar, and command line.
|
|
||||||
func (m Model) View() string {
|
|
||||||
win := m.ActiveWindow()
|
|
||||||
|
|
||||||
// NOTES:
|
|
||||||
// One single command line across entire viewport
|
|
||||||
// Each window has its own line numbers and gutter
|
|
||||||
// Each window has its own status bar and mode
|
|
||||||
|
|
||||||
styles := m.Styles()
|
|
||||||
options := win.Options
|
|
||||||
|
|
||||||
// Draw window
|
|
||||||
view := viewWindow(win, styles, options, m.Mode())
|
|
||||||
|
|
||||||
// Command bar is seperate
|
|
||||||
cmdBar := drawCommandBar(m)
|
|
||||||
return view + cmdBar
|
|
||||||
}
|
|
||||||
|
|
||||||
// viewWindow: Renders a single window's content including line numbers and buffer text.
|
|
||||||
// Each window has its own line numbers, gutter, and viewport dimensions.
|
|
||||||
func viewWindow(w *core.Window, styles style.Styles, options core.WinOptions, mode core.Mode) string {
|
|
||||||
buf := w.Buffer
|
|
||||||
var view strings.Builder
|
|
||||||
|
|
||||||
// Compute window size (y)
|
|
||||||
start := w.ScrollY
|
|
||||||
end := w.ScrollY + w.ViewportHeight()
|
|
||||||
|
|
||||||
// Draw buffer lines
|
|
||||||
for lineNum := start; lineNum < end; lineNum++ {
|
|
||||||
if lineNum < buf.LineCount() {
|
|
||||||
line := drawLine(w, styles, options, mode, buf.Line(lineNum), lineNum)
|
|
||||||
view.WriteString(line)
|
|
||||||
}
|
|
||||||
view.WriteRune('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw status line
|
|
||||||
statusBar := drawStatusBar(w, mode)
|
|
||||||
view.WriteString(statusBar + "\n")
|
|
||||||
|
|
||||||
return view.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawLine: Renders a single line with syntax highlighting, cursor, and visual selection.
|
|
||||||
// Handles gutter, cursor rendering, and visual mode highlighting.
|
|
||||||
func drawLine(w *core.Window, styles style.Styles, options core.WinOptions, mode core.Mode, line string, lineNumber int) string {
|
|
||||||
runes := []rune(line)
|
|
||||||
|
|
||||||
curStyle := styles.CursorStyle(mode)
|
|
||||||
visStyle := styles.VisualHighlight
|
|
||||||
|
|
||||||
var view strings.Builder
|
|
||||||
|
|
||||||
// Draw gutter first
|
|
||||||
gutter := drawGutter(w, styles, options, lineNumber)
|
|
||||||
view.WriteString(gutter)
|
|
||||||
|
|
||||||
// Now draw the line content
|
|
||||||
for col := 0; col <= len(runes); col++ {
|
|
||||||
// Current char is cursor
|
|
||||||
if col == w.Cursor.Col && lineNumber == w.Cursor.Line {
|
|
||||||
if col < len(runes) {
|
|
||||||
view.WriteString(curStyle.Render(string(runes[col])))
|
|
||||||
} else {
|
|
||||||
view.WriteString(curStyle.Render(" "))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not cursor, but not end
|
|
||||||
} else if col < len(runes) {
|
|
||||||
if mode.IsVisualMode() && posInsideSelection(w, mode, col, lineNumber) {
|
|
||||||
view.WriteString(visStyle.Render(string(runes[col])))
|
|
||||||
} else {
|
|
||||||
view.WriteRune(runes[col])
|
|
||||||
}
|
|
||||||
// Allow highlight on blank lines or chars
|
|
||||||
} else if mode.IsVisualMode() && posInsideSelection(w, mode, col, lineNumber) {
|
|
||||||
view.WriteString(visStyle.Render(" "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return view.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawGutter: Renders the line number gutter with support for both absolute and
|
|
||||||
// relative line numbers, highlighting the current line differently.
|
|
||||||
func drawGutter(w *core.Window, styles style.Styles, options core.WinOptions, curLine int) string {
|
|
||||||
if !(options.Number || options.RelativeNumber) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Required vars
|
|
||||||
var (
|
|
||||||
view strings.Builder
|
|
||||||
gutSize int = options.GutterSize - 1 // -1 is for padding
|
|
||||||
currentLine bool = curLine == w.Cursor.Line
|
|
||||||
lineNumber int
|
|
||||||
|
|
||||||
gutter string
|
|
||||||
gutterStyle = styles.Gutter
|
|
||||||
gutterStyleCur = styles.GutterCurrentLine
|
|
||||||
)
|
|
||||||
|
|
||||||
// If we have relative setting, set the numbers relatively
|
|
||||||
if options.RelativeNumber {
|
|
||||||
if curLine > w.Cursor.Line {
|
|
||||||
lineNumber = curLine - w.Cursor.Line
|
|
||||||
}
|
|
||||||
|
|
||||||
if curLine < w.Cursor.Line {
|
|
||||||
lineNumber = w.Cursor.Line - curLine
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have number setting AND not relative setting OR we are on current line, use current line number
|
|
||||||
if (options.Number && !options.RelativeNumber) || (options.Number && currentLine) {
|
|
||||||
lineNumber = curLine + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw the gutter
|
|
||||||
gutter = fmt.Sprintf("%*d ", gutSize, lineNumber)
|
|
||||||
if currentLine {
|
|
||||||
view.WriteString(gutterStyleCur.Render(gutter))
|
|
||||||
} else {
|
|
||||||
view.WriteString(gutterStyle.Render(gutter))
|
|
||||||
}
|
|
||||||
|
|
||||||
return view.String()
|
|
||||||
|
|
||||||
// if m.Settings().Number || m.Settings().RelativeNumber {
|
|
||||||
// var (
|
|
||||||
// gutter string
|
|
||||||
// currentLine bool = false
|
|
||||||
// lineNumber int
|
|
||||||
// )
|
|
||||||
//
|
|
||||||
// if m.Settings().RelativeNumber {
|
|
||||||
// // Relative line numbers: show distance from cursor, current line shows absolute
|
|
||||||
// if i > win.Cursor.Line {
|
|
||||||
// lineNumber = i - win.Cursor.Line
|
|
||||||
// gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
|
||||||
// } else if i < win.Cursor.Line {
|
|
||||||
// lineNumber = win.Cursor.Line - i
|
|
||||||
// gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
|
||||||
// } else {
|
|
||||||
// // Current line: show absolute number if Number is also set, otherwise show 0
|
|
||||||
// currentLine = true
|
|
||||||
// if m.Settings().Number {
|
|
||||||
// lineNumber = i + 1
|
|
||||||
// gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
|
||||||
// } else {
|
|
||||||
// gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, 0)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } else if m.Settings().Number {
|
|
||||||
// // Absolute line numbers only
|
|
||||||
// lineNumber = i + 1
|
|
||||||
// currentLine = (i == win.Cursor.Line)
|
|
||||||
// gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
|
||||||
// }
|
|
||||||
// if currentLine {
|
|
||||||
// view.WriteString(m.Styles().GutterCurrentLine.Render(gutter))
|
|
||||||
// } else {
|
|
||||||
// view.WriteString(m.Styles().Gutter.Render(gutter))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawStatusBar: Renders the status bar with mode and cursor position,
|
|
||||||
// padding the middle with spaces to fill the terminal width.
|
|
||||||
func drawStatusBar(w *core.Window, mode core.Mode) string {
|
|
||||||
left := leftBar(w, mode)
|
|
||||||
right := rightBar(w, mode)
|
|
||||||
|
|
||||||
diff := w.Width - (len(left) + len(right))
|
|
||||||
|
|
||||||
// This happens when the terminal spawns
|
|
||||||
if diff <= 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
middle := strings.Repeat(" ", diff)
|
|
||||||
return left + middle + right
|
|
||||||
}
|
|
||||||
|
|
||||||
// leftBar: Returns the left side of the status bar showing the current mode.
|
|
||||||
func leftBar(w *core.Window, mode core.Mode) string {
|
|
||||||
buf := w.Buffer
|
|
||||||
return fmt.Sprintf(" %s %s", mode.ToString(), buf.Filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
// rightBar: Returns the right side of the status bar showing cursor position
|
|
||||||
// and selection count in visual mode.
|
|
||||||
func rightBar(w *core.Window, mode core.Mode) (bar string) {
|
|
||||||
if mode.IsVisualMode() {
|
|
||||||
lineCount := max(w.Anchor.Line, w.Cursor.Line) - min(w.Anchor.Line, w.Cursor.Line) + 1
|
|
||||||
bar = fmt.Sprintf("%d:%d <%d>", w.Cursor.Line+1, w.Cursor.Col+1, lineCount)
|
|
||||||
} else {
|
|
||||||
bar = fmt.Sprintf("%d:%d ", w.Cursor.Line+1, w.Cursor.Col+1)
|
|
||||||
}
|
|
||||||
buf := w.Buffer
|
|
||||||
bar = fmt.Sprintf("%s %s", buf.Filetype, bar)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawCommandBar: Renders the command line showing command input, errors, or
|
|
||||||
// output depending on the current mode and state.
|
|
||||||
func drawCommandBar(m Model) string {
|
|
||||||
// Compute left bar (command side)
|
|
||||||
var leftBar string
|
|
||||||
if m.Mode() == core.CommandMode {
|
|
||||||
leftBar = ":"
|
|
||||||
cmd := m.Command()
|
|
||||||
cur := m.CommandCursor()
|
|
||||||
for i := 0; i < len(cmd); i++ {
|
|
||||||
if i == cur {
|
|
||||||
leftBar += m.Styles().CursorStyle(m.Mode()).Render(string(cmd[i]))
|
|
||||||
} else {
|
|
||||||
leftBar += string(cmd[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Cursor at end of command
|
|
||||||
if cur >= len(cmd) {
|
|
||||||
leftBar += m.Styles().CursorStyle(m.Mode()).Render(" ")
|
|
||||||
}
|
|
||||||
// bar = fmt.Sprintf("%s %d", bar, cur)
|
|
||||||
} else if m.CommandError() != nil {
|
|
||||||
leftBar = m.Styles().CommandError.Render(m.CommandError().Error())
|
|
||||||
} else if strings.TrimSpace(m.CommandOutput()) != "" {
|
|
||||||
leftBar = m.CommandOutput()
|
|
||||||
} else if strings.TrimSpace(m.Command()) != "" {
|
|
||||||
leftBar = fmt.Sprintf(":%s", m.Command())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute right bar
|
|
||||||
|
|
||||||
var rightBar string
|
|
||||||
if len(m.input.Pending()) > 0 {
|
|
||||||
width := 10 // Size of the block to display
|
|
||||||
rightBar = fmt.Sprintf("%-*s", width, m.input.Pending())
|
|
||||||
}
|
|
||||||
|
|
||||||
dif := m.termWidth - (len(leftBar) + len(rightBar))
|
|
||||||
|
|
||||||
bar := leftBar + strings.Repeat(" ", dif) + rightBar
|
|
||||||
return bar
|
|
||||||
}
|
|
||||||
|
|
||||||
// posInsideSelection: Returns true if the given position is inside the current
|
// posInsideSelection: Returns true if the given position is inside the current
|
||||||
// visual selection, handling all three visual modes differently.
|
// visual selection, handling all three visual modes differently.
|
||||||
func posInsideSelection(w *core.Window, mode core.Mode, col, line int) bool {
|
func posInsideSelection(m Model, col, line int) bool {
|
||||||
switch mode {
|
win := m.ActiveWindow()
|
||||||
|
|
||||||
|
switch m.Mode() {
|
||||||
case core.VisualLineMode:
|
case core.VisualLineMode:
|
||||||
startY := min(w.Anchor.Line, w.Cursor.Line)
|
startY := min(win.Anchor.Line, win.Cursor.Line)
|
||||||
endY := max(w.Anchor.Line, w.Cursor.Line)
|
endY := max(win.Anchor.Line, win.Cursor.Line)
|
||||||
return line >= startY && line <= endY
|
return line >= startY && line <= endY
|
||||||
|
|
||||||
case core.VisualMode:
|
case core.VisualMode:
|
||||||
ax := w.Anchor.Col
|
ax := win.Anchor.Col
|
||||||
ay := w.Anchor.Line
|
ay := win.Anchor.Line
|
||||||
|
|
||||||
cx := w.Cursor.Col
|
cx := win.Cursor.Col
|
||||||
cy := w.Cursor.Line
|
cy := win.Cursor.Line
|
||||||
|
|
||||||
// Normalize so start is always before end in document order
|
// Normalize so start is always before end in document order
|
||||||
var startX, startY, endX, endY int
|
var startX, startY, endX, endY int
|
||||||
@ -291,10 +41,10 @@ func posInsideSelection(w *core.Window, mode core.Mode, col, line int) bool {
|
|||||||
return afterStart && beforeEnd
|
return afterStart && beforeEnd
|
||||||
|
|
||||||
case core.VisualBlockMode:
|
case core.VisualBlockMode:
|
||||||
startX := min(w.Anchor.Col, w.Cursor.Col)
|
startX := min(win.Anchor.Col, win.Cursor.Col)
|
||||||
startY := min(w.Anchor.Line, w.Cursor.Line)
|
startY := min(win.Anchor.Line, win.Cursor.Line)
|
||||||
endX := max(w.Anchor.Col, w.Cursor.Col)
|
endX := max(win.Anchor.Col, win.Cursor.Col)
|
||||||
endY := max(w.Anchor.Line, w.Cursor.Line)
|
endY := max(win.Anchor.Line, win.Cursor.Line)
|
||||||
|
|
||||||
return col >= startX && col <= endX &&
|
return col >= startX && col <= endX &&
|
||||||
line >= startY && line <= endY
|
line >= startY && line <= endY
|
||||||
@ -303,3 +53,183 @@ func posInsideSelection(w *core.Window, mode core.Mode, col, line int) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// posIsAnchor: Returns true if the given position matches the anchor position
|
||||||
|
// used for visual mode debugging/rendering.
|
||||||
|
func posIsAnchor(m Model, col, line int) bool {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
ax := win.Anchor.Col
|
||||||
|
ay := win.Anchor.Line
|
||||||
|
return col == ax && line == ay
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model.View: Renders the complete editor view including buffer content, line
|
||||||
|
// numbers, status bar, and command line.
|
||||||
|
func (m Model) View() string {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
buf := m.ActiveBuffer()
|
||||||
|
|
||||||
|
var view strings.Builder
|
||||||
|
|
||||||
|
viewportHeight := win.Height - 2
|
||||||
|
start := win.ScrollY
|
||||||
|
end := win.ScrollY + viewportHeight
|
||||||
|
|
||||||
|
for i := start; i < end; i++ {
|
||||||
|
|
||||||
|
if i < buf.LineCount() {
|
||||||
|
|
||||||
|
if m.Settings().Number || m.Settings().RelativeNumber {
|
||||||
|
var (
|
||||||
|
gutter string
|
||||||
|
currentLine bool = false
|
||||||
|
lineNumber int
|
||||||
|
)
|
||||||
|
|
||||||
|
if m.Settings().RelativeNumber {
|
||||||
|
// Relative line numbers: show distance from cursor, current line shows absolute
|
||||||
|
if i > win.Cursor.Line {
|
||||||
|
lineNumber = i - win.Cursor.Line
|
||||||
|
gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
||||||
|
} else if i < win.Cursor.Line {
|
||||||
|
lineNumber = win.Cursor.Line - i
|
||||||
|
gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
||||||
|
} else {
|
||||||
|
// Current line: show absolute number if Number is also set, otherwise show 0
|
||||||
|
currentLine = true
|
||||||
|
if m.Settings().Number {
|
||||||
|
lineNumber = i + 1
|
||||||
|
gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
||||||
|
} else {
|
||||||
|
gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if m.Settings().Number {
|
||||||
|
// Absolute line numbers only
|
||||||
|
lineNumber = i + 1
|
||||||
|
currentLine = (i == win.Cursor.Line)
|
||||||
|
gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
|
||||||
|
}
|
||||||
|
if currentLine {
|
||||||
|
view.WriteString(m.Styles().GutterCurrentLine.Render(gutter))
|
||||||
|
} else {
|
||||||
|
view.WriteString(m.Styles().Gutter.Render(gutter))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runes := []rune(buf.Lines[i])
|
||||||
|
for x := 0; x <= len(runes); x++ {
|
||||||
|
if win.Cursor.Line == i && win.Cursor.Col == x {
|
||||||
|
if x < len(runes) {
|
||||||
|
view.WriteString(m.Styles().CursorStyle(m.Mode()).Render(string(runes[x])))
|
||||||
|
} else {
|
||||||
|
view.WriteString(m.Styles().CursorStyle(m.Mode()).Render(" "))
|
||||||
|
}
|
||||||
|
} else if x < len(runes) {
|
||||||
|
if m.Mode().IsVisualMode() && posIsAnchor(m, x, i) {
|
||||||
|
view.WriteString(m.Styles().VisualAnchor.Render(string(runes[x])))
|
||||||
|
} else if m.Mode().IsVisualMode() && posInsideSelection(m, x, i) {
|
||||||
|
view.WriteString(m.Styles().VisualHighlight.Render(string(runes[x])))
|
||||||
|
} else {
|
||||||
|
view.WriteRune(runes[x])
|
||||||
|
}
|
||||||
|
// To highlight blank lines when in visual mode
|
||||||
|
} else if m.Mode().IsVisualMode() && posInsideSelection(m, x, i) {
|
||||||
|
view.WriteString(m.Styles().VisualHighlight.Render(" "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Empty lines beyond file content
|
||||||
|
if m.Settings().Number || m.Settings().RelativeNumber {
|
||||||
|
format := fmt.Sprintf("%%-%ds ", m.Settings().GutterSize-1)
|
||||||
|
fmt.Fprintf(&view, format, "~")
|
||||||
|
} else {
|
||||||
|
view.WriteString("~")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
view.WriteString(drawStatusBar(m))
|
||||||
|
view.WriteString("\n")
|
||||||
|
view.WriteString(drawCommandBar(m))
|
||||||
|
|
||||||
|
return view.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func viewWindow(w core.Window) string {
|
||||||
|
var view string
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawStatusBar: Renders the status bar with mode and cursor position,
|
||||||
|
// padding the middle with spaces to fill the terminal width.
|
||||||
|
func drawStatusBar(m Model) string {
|
||||||
|
left := leftBar(m)
|
||||||
|
right := rightBar(m)
|
||||||
|
|
||||||
|
diff := m.termWidth - (len(left) + len(right))
|
||||||
|
|
||||||
|
// This happens when the terminal spawns
|
||||||
|
if diff <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
middle := strings.Repeat(" ", diff)
|
||||||
|
return left + middle + right
|
||||||
|
}
|
||||||
|
|
||||||
|
// leftBar: Returns the left side of the status bar showing the current mode.
|
||||||
|
func leftBar(m Model) string {
|
||||||
|
return fmt.Sprintf(" %s", m.Mode().ToString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// rightBar: Returns the right side of the status bar showing cursor position
|
||||||
|
// and selection count in visual mode.
|
||||||
|
func rightBar(m Model) (bar string) {
|
||||||
|
win := m.ActiveWindow()
|
||||||
|
|
||||||
|
if m.Mode().IsVisualMode() {
|
||||||
|
lineCount := max(win.Anchor.Line, win.Cursor.Line) - min(win.Anchor.Line, win.Cursor.Line) + 1
|
||||||
|
bar = fmt.Sprintf("%d:%d <%d>", win.Cursor.Line, win.Cursor.Col, lineCount)
|
||||||
|
} else {
|
||||||
|
bar = fmt.Sprintf("%d:%d ", win.Cursor.Line, win.Cursor.Col)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawCommandBar: Renders the command line showing command input, errors, or
|
||||||
|
// output depending on the current mode and state.
|
||||||
|
func drawCommandBar(m Model) (bar string) {
|
||||||
|
if m.Mode() == core.CommandMode {
|
||||||
|
bar = ":"
|
||||||
|
cmd := m.Command()
|
||||||
|
cur := m.CommandCursor()
|
||||||
|
for i := 0; i < len(cmd); i++ {
|
||||||
|
if i == cur {
|
||||||
|
bar += m.Styles().CursorStyle(m.Mode()).Render(string(cmd[i]))
|
||||||
|
} else {
|
||||||
|
bar += string(cmd[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cursor at end of command
|
||||||
|
if cur >= len(cmd) {
|
||||||
|
bar += m.Styles().CursorStyle(m.Mode()).Render(" ")
|
||||||
|
}
|
||||||
|
// bar = fmt.Sprintf("%s %d", bar, cur)
|
||||||
|
} else if m.CommandError() != nil {
|
||||||
|
bar = m.Styles().CommandError.Render(m.CommandError().Error())
|
||||||
|
} else if strings.TrimSpace(m.CommandOutput()) != "" {
|
||||||
|
bar = m.CommandOutput()
|
||||||
|
} else if strings.TrimSpace(m.Command()) != "" {
|
||||||
|
bar = fmt.Sprintf(":%s", m.Command())
|
||||||
|
} else if len(m.input.Pending()) > 0 {
|
||||||
|
// Get width of window and padding
|
||||||
|
rep := m.ActiveWindow().Width - 10 // 10 is padding
|
||||||
|
bar = fmt.Sprintf("%s%s", strings.Repeat(" ", rep), m.input.Pending())
|
||||||
|
}
|
||||||
|
|
||||||
|
return bar
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user