Compare commits
5 Commits
6034e44364
...
be13f8838d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be13f8838d | ||
|
|
1c2585b8d9 | ||
|
|
77416bc0a4 | ||
|
|
760770c564 | ||
|
|
76f949a6b2 |
@ -4,17 +4,12 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/program"
|
"git.gophernest.net/azpect/TextEditor/internal/program"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/theme"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
// main: Entry point for the Gim text editor. Creates a buffer and window,
|
// main: Entry point for the Gim text editor. Creates a buffer and window,
|
||||||
// initializes the editor model, and runs the BubbleTea TUI program.
|
// initializes the editor model, and runs the BubbleTea TUI program.
|
||||||
func main() {
|
func main() {
|
||||||
if err := theme.RegisterAll(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// <exe> <filename>
|
// <exe> <filename>
|
||||||
args := os.Args[1:]
|
args := os.Args[1:]
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package action
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/style"
|
"git.gophernest.net/azpect/TextEditor/internal/theme"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -54,8 +54,8 @@ type Model interface {
|
|||||||
|
|
||||||
Settings() core.EditorSettings
|
Settings() core.EditorSettings
|
||||||
SetSettings(s core.EditorSettings)
|
SetSettings(s core.EditorSettings)
|
||||||
Styles() style.Styles
|
Theme() theme.EditorTheme
|
||||||
SetStyles(s style.Styles)
|
SetTheme(t theme.EditorTheme)
|
||||||
|
|
||||||
// ==================================================
|
// ==================================================
|
||||||
// Registers
|
// Registers
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package action
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/style"
|
"git.gophernest.net/azpect/TextEditor/internal/theme"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ type MockModel struct {
|
|||||||
CommandHistoryList []string
|
CommandHistoryList []string
|
||||||
CommandHistoryCur int
|
CommandHistoryCur int
|
||||||
LastFindVal core.LastFindCommand
|
LastFindVal core.LastFindCommand
|
||||||
StylesVal style.Styles
|
ThemeVal theme.EditorTheme
|
||||||
LastChangeKeysList []string
|
LastChangeKeysList []string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,8 +117,8 @@ func (m *MockModel) Mode() core.Mode { return m.ModeVal }
|
|||||||
func (m *MockModel) SetMode(mode core.Mode) { m.ModeVal = mode }
|
func (m *MockModel) SetMode(mode core.Mode) { m.ModeVal = mode }
|
||||||
func (m *MockModel) Settings() core.EditorSettings { return m.SettingsVal }
|
func (m *MockModel) Settings() core.EditorSettings { return m.SettingsVal }
|
||||||
func (m *MockModel) SetSettings(s core.EditorSettings) { m.SettingsVal = s }
|
func (m *MockModel) SetSettings(s core.EditorSettings) { m.SettingsVal = s }
|
||||||
func (m *MockModel) Styles() style.Styles { return m.StylesVal }
|
func (m *MockModel) Theme() theme.EditorTheme { return m.ThemeVal }
|
||||||
func (m *MockModel) SetStyles(s style.Styles) { m.StylesVal = s }
|
func (m *MockModel) SetTheme(t theme.EditorTheme) { m.ThemeVal = t }
|
||||||
|
|
||||||
// Registers
|
// Registers
|
||||||
func (m *MockModel) Registers() map[rune]core.Register { return m.RegistersMap }
|
func (m *MockModel) Registers() map[rune]core.Register { return m.RegistersMap }
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import (
|
|||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/style"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -915,7 +914,7 @@ func cmdColorscheme(m action.Model, args []string, force bool) tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SetStyles(style.DefaultStyles())
|
// m.SetStyles(style.DefaultStyles())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/style"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -5512,32 +5511,32 @@ func TestCmdColorscheme(t *testing.T) {
|
|||||||
// Group 1: Valid name — styles are updated
|
// Group 1: Valid name — styles are updated
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
|
||||||
t.Run("valid name updates styles on model", func(t *testing.T) {
|
// t.Run("valid name updates styles on model", func(t *testing.T) {
|
||||||
m := action.NewMockModel()
|
// m := action.NewMockModel()
|
||||||
m.SetStyles(style.DefaultStyles())
|
// // m.SetStyles(style.DefaultStyles())
|
||||||
before := m.StylesVal.BackgroundStyle.Render(" ")
|
// before := m.StylesVal.BackgroundStyle.Render(" ")
|
||||||
|
//
|
||||||
cmdColorscheme(m, []string{"default"}, false)
|
// cmdColorscheme(m, []string{"default"}, false)
|
||||||
|
//
|
||||||
after := m.StylesVal.BackgroundStyle.Render(" ")
|
// after := m.StylesVal.BackgroundStyle.Render(" ")
|
||||||
if after != before {
|
// if after != before {
|
||||||
t.Error("expected default styles to remain stable after applying default")
|
// t.Error("expected default styles to remain stable after applying default")
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
|
//
|
||||||
t.Run("same valid name applied twice produces same styles", func(t *testing.T) {
|
// t.Run("same valid name applied twice produces same styles", func(t *testing.T) {
|
||||||
m := action.NewMockModel()
|
// m := action.NewMockModel()
|
||||||
|
//
|
||||||
cmdColorscheme(m, []string{"default"}, false)
|
// cmdColorscheme(m, []string{"default"}, false)
|
||||||
first := m.StylesVal.BackgroundStyle.Render(" ")
|
// first := m.StylesVal.BackgroundStyle.Render(" ")
|
||||||
|
//
|
||||||
cmdColorscheme(m, []string{"default"}, false)
|
// cmdColorscheme(m, []string{"default"}, false)
|
||||||
second := m.StylesVal.BackgroundStyle.Render(" ")
|
// second := m.StylesVal.BackgroundStyle.Render(" ")
|
||||||
|
//
|
||||||
if first != second {
|
// if first != second {
|
||||||
t.Error("expected applying the same colorscheme twice to produce identical styles")
|
// t.Error("expected applying the same colorscheme twice to produce identical styles")
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
|
|
||||||
t.Run("valid name sets no error output", func(t *testing.T) {
|
t.Run("valid name sets no error output", func(t *testing.T) {
|
||||||
m := action.NewMockModel()
|
m := action.NewMockModel()
|
||||||
@ -5576,16 +5575,16 @@ func TestCmdColorscheme(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unknown name does not change styles", func(t *testing.T) {
|
// t.Run("unknown name does not change styles", func(t *testing.T) {
|
||||||
m := action.NewMockModel()
|
// m := action.NewMockModel()
|
||||||
before := m.StylesVal.BackgroundStyle.Render(" ")
|
// before := m.StylesVal.BackgroundStyle.Render(" ")
|
||||||
|
//
|
||||||
cmdColorscheme(m, []string{"not-a-real-theme"}, false)
|
// cmdColorscheme(m, []string{"not-a-real-theme"}, false)
|
||||||
|
//
|
||||||
if m.StylesVal.BackgroundStyle.Render(" ") != before {
|
// if m.StylesVal.BackgroundStyle.Render(" ") != before {
|
||||||
t.Error("expected styles to remain unchanged after unknown colorscheme")
|
// t.Error("expected styles to remain unchanged after unknown colorscheme")
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
|
|
||||||
t.Run("empty string name sets error output", func(t *testing.T) {
|
t.Run("empty string name sets error output", func(t *testing.T) {
|
||||||
m := action.NewMockModel()
|
m := action.NewMockModel()
|
||||||
@ -5624,16 +5623,16 @@ func TestCmdColorscheme(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("no args does not change styles", func(t *testing.T) {
|
// t.Run("no args does not change styles", func(t *testing.T) {
|
||||||
m := action.NewMockModel()
|
// m := action.NewMockModel()
|
||||||
before := m.StylesVal.BackgroundStyle.Render(" ")
|
// before := m.StylesVal.BackgroundStyle.Render(" ")
|
||||||
|
//
|
||||||
cmdColorscheme(m, []string{}, false)
|
// cmdColorscheme(m, []string{}, false)
|
||||||
|
//
|
||||||
if m.StylesVal.BackgroundStyle.Render(" ") != before {
|
// if m.StylesVal.BackgroundStyle.Render(" ") != before {
|
||||||
t.Error("expected styles to remain unchanged when no args given")
|
// t.Error("expected styles to remain unchanged when no args given")
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
|
|
||||||
t.Run("no args returns nil tea.Cmd", func(t *testing.T) {
|
t.Run("no args returns nil tea.Cmd", func(t *testing.T) {
|
||||||
m := action.NewMockModel()
|
m := action.NewMockModel()
|
||||||
@ -5802,5 +5801,3 @@ func TestCmdListColorschemes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ = style.DefaultStyles
|
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import (
|
|||||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/input"
|
"git.gophernest.net/azpect/TextEditor/internal/input"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/style"
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/syntax"
|
"git.gophernest.net/azpect/TextEditor/internal/syntax"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/theme"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ type Model struct {
|
|||||||
registers map[rune]core.Register // name -> register
|
registers map[rune]core.Register // name -> register
|
||||||
|
|
||||||
// Visual styles
|
// Visual styles
|
||||||
styles style.Styles
|
theme theme.EditorTheme
|
||||||
syntax syntax.Engine
|
syntax syntax.Engine
|
||||||
|
|
||||||
// Dot operator state
|
// Dot operator state
|
||||||
@ -341,14 +341,12 @@ func (m *Model) SetSettings(s core.EditorSettings) {
|
|||||||
m.settings = s
|
m.settings = s
|
||||||
}
|
}
|
||||||
|
|
||||||
// Model.Styles: Returns the visual styles used for rendering.
|
func (m *Model) Theme() theme.EditorTheme {
|
||||||
func (m *Model) Styles() style.Styles {
|
return m.theme
|
||||||
return m.styles
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Model.SetStyles: Sets the visual styles used for rendering.
|
func (m *Model) SetTheme(t theme.EditorTheme) {
|
||||||
func (m *Model) SetStyles(s style.Styles) {
|
m.theme = t
|
||||||
m.styles = s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) Syntax() syntax.Engine {
|
func (m *Model) Syntax() syntax.Engine {
|
||||||
|
|||||||
@ -3,8 +3,8 @@ package editor
|
|||||||
import (
|
import (
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/input"
|
"git.gophernest.net/azpect/TextEditor/internal/input"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/style"
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/syntax"
|
"git.gophernest.net/azpect/TextEditor/internal/syntax"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/theme/themes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelBuilder struct {
|
type ModelBuilder struct {
|
||||||
@ -13,7 +13,7 @@ type ModelBuilder struct {
|
|||||||
|
|
||||||
// NewModelBuilder: Builds and returns a new model, using the default color scheme (kanagawa-wave).
|
// NewModelBuilder: Builds and returns a new model, using the default color scheme (kanagawa-wave).
|
||||||
func NewModelBuilder() *ModelBuilder {
|
func NewModelBuilder() *ModelBuilder {
|
||||||
editorStyles := style.DefaultStyles()
|
editorTheme := themes.NewDefaultTheme()
|
||||||
|
|
||||||
return &ModelBuilder{
|
return &ModelBuilder{
|
||||||
model: Model{
|
model: Model{
|
||||||
@ -32,8 +32,8 @@ func NewModelBuilder() *ModelBuilder {
|
|||||||
commandOutput: nil,
|
commandOutput: nil,
|
||||||
settings: core.NewDefaultSettings(),
|
settings: core.NewDefaultSettings(),
|
||||||
registers: core.DefaultRegisters(),
|
registers: core.DefaultRegisters(),
|
||||||
styles: editorStyles,
|
syntax: syntax.NewTreeSitterEngine(editorTheme),
|
||||||
syntax: syntax.NewTreeSitterEngine(editorStyles),
|
theme: editorTheme,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,12 +126,6 @@ func (mb *ModelBuilder) WithCommandOutput(out *core.CommandOutput) *ModelBuilder
|
|||||||
return mb
|
return mb
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModelBuilder.WithStyles: Set the visual styling for the editor.
|
|
||||||
func (mb *ModelBuilder) WithStyles(styles style.Styles) *ModelBuilder {
|
|
||||||
mb.model.styles = styles
|
|
||||||
return mb
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModelBuilder.Build: Build and return the configured Model instance.
|
// ModelBuilder.Build: Build and return the configured Model instance.
|
||||||
func (mb *ModelBuilder) Build() *Model {
|
func (mb *ModelBuilder) Build() *Model {
|
||||||
m := &mb.model
|
m := &mb.model
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/style"
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/syntax"
|
"git.gophernest.net/azpect/TextEditor/internal/syntax"
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/theme"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ func (m Model) View() string {
|
|||||||
// Each window has its own line numbers and gutter
|
// Each window has its own line numbers and gutter
|
||||||
// Each window has its own status bar and mode
|
// Each window has its own status bar and mode
|
||||||
|
|
||||||
styles := m.Styles()
|
t := m.Theme()
|
||||||
options := win.Options
|
options := win.Options
|
||||||
|
|
||||||
// Adjust gutter to fit line len
|
// Adjust gutter to fit line len
|
||||||
@ -29,17 +29,17 @@ func (m Model) View() string {
|
|||||||
options.GutterSize = max(options.GutterSize, maxLineLen+2)
|
options.GutterSize = max(options.GutterSize, maxLineLen+2)
|
||||||
|
|
||||||
// Draw window
|
// Draw window
|
||||||
view := viewWindow(win, styles, options, m.Mode(), m.Syntax())
|
view := viewWindow(win, t, options, m.Mode(), m.Syntax())
|
||||||
|
|
||||||
// Command bar is seperate
|
// Command bar is separate
|
||||||
cmdBar := drawCommandBar(m)
|
cmdBar := drawCommandBar(m, t)
|
||||||
view += cmdBar
|
view += cmdBar
|
||||||
|
|
||||||
// Handle command output, draw on top
|
// Handle command output, draw on top
|
||||||
// TODO: This is not idea, but it works for now
|
// TODO: This is not idea, but it works for now
|
||||||
cmd := m.CommandOutput()
|
cmd := m.CommandOutput()
|
||||||
if cmd != nil && cmd.IsActive() && !cmd.Inline && cmd.Height() > 0 {
|
if cmd != nil && cmd.IsActive() && !cmd.Inline && cmd.Height() > 0 {
|
||||||
view = overlayCommandOutputWindow(view, cmd, styles, m.termWidth, m.termHeight)
|
view = overlayCommandOutputWindow(view, cmd, t, m.termWidth, m.termHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
return view
|
return view
|
||||||
@ -47,7 +47,7 @@ func (m Model) View() string {
|
|||||||
|
|
||||||
// viewWindow: Renders a single window's content including line numbers and buffer text.
|
// viewWindow: Renders a single window's content including line numbers and buffer text.
|
||||||
// Each window has its own line numbers, gutter, and viewport dimensions.
|
// 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, sx syntax.Engine) string {
|
func viewWindow(w *core.Window, t theme.EditorTheme, options core.WinOptions, mode core.Mode, sx syntax.Engine) string {
|
||||||
buf := w.Buffer
|
buf := w.Buffer
|
||||||
var view strings.Builder
|
var view strings.Builder
|
||||||
if sx != nil {
|
if sx != nil {
|
||||||
@ -65,16 +65,16 @@ func viewWindow(w *core.Window, styles style.Styles, options core.WinOptions, mo
|
|||||||
if sx != nil {
|
if sx != nil {
|
||||||
styleMap = sx.LineStyleMap(buf, lineNum)
|
styleMap = sx.LineStyleMap(buf, lineNum)
|
||||||
}
|
}
|
||||||
line := drawLine(w, styles, options, mode, buf.Line(lineNum), lineNum, styleMap)
|
line := drawLine(w, t, options, mode, buf.Line(lineNum), lineNum, styleMap)
|
||||||
view.WriteString(line)
|
view.WriteString(line)
|
||||||
} else {
|
} else {
|
||||||
view.WriteString(strings.Repeat(styles.BackgroundStyle.Render(" "), w.Width))
|
view.WriteString(strings.Repeat(t.Background.Render(" "), w.Width))
|
||||||
}
|
}
|
||||||
view.WriteRune('\n')
|
view.WriteRune('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw status line
|
// Draw status line
|
||||||
statusBar := drawStatusBar(w, mode, styles)
|
statusBar := drawStatusBar(w, mode, t)
|
||||||
view.WriteString(statusBar + "\n")
|
view.WriteString(statusBar + "\n")
|
||||||
|
|
||||||
return view.String()
|
return view.String()
|
||||||
@ -82,12 +82,12 @@ func viewWindow(w *core.Window, styles style.Styles, options core.WinOptions, mo
|
|||||||
|
|
||||||
// drawLine: Renders a single line with syntax highlighting, cursor, and visual selection.
|
// drawLine: Renders a single line with syntax highlighting, cursor, and visual selection.
|
||||||
// Handles gutter, cursor rendering, and visual mode highlighting.
|
// 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, styleMap []lipgloss.Style) string {
|
func drawLine(w *core.Window, t theme.EditorTheme, options core.WinOptions, mode core.Mode, line string, lineNumber int, styleMap []lipgloss.Style) string {
|
||||||
var view strings.Builder
|
var view strings.Builder
|
||||||
runes := []rune(line)
|
runes := []rune(line)
|
||||||
|
|
||||||
// Draw gutter first
|
// Draw gutter first
|
||||||
gutter := drawGutter(w, styles, options, lineNumber)
|
gutter := drawGutter(w, t, options, lineNumber)
|
||||||
view.WriteString(gutter)
|
view.WriteString(gutter)
|
||||||
|
|
||||||
// Now draw the line content
|
// Now draw the line content
|
||||||
@ -95,31 +95,31 @@ func drawLine(w *core.Window, styles style.Styles, options core.WinOptions, mode
|
|||||||
// Current char is cursor
|
// Current char is cursor
|
||||||
if col == w.Cursor.Col && lineNumber == w.Cursor.Line {
|
if col == w.Cursor.Col && lineNumber == w.Cursor.Line {
|
||||||
if col < len(runes) {
|
if col < len(runes) {
|
||||||
cur := styles.CursorStyle(mode, styleMap[col])
|
cur := t.Cursor(mode, styleMap[col])
|
||||||
view.WriteString(cur.Render(string(runes[col])))
|
view.WriteString(cur.Render(string(runes[col])))
|
||||||
} else {
|
} else {
|
||||||
view.WriteString(styles.DefaultCursorStyle(mode).Render(" "))
|
view.WriteString(t.DefaultCursor(mode).Render(" "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not cursor, but not end
|
// Not cursor, but not end
|
||||||
} else if col < len(runes) {
|
} else if col < len(runes) {
|
||||||
s := styleMap[col]
|
s := styleMap[col]
|
||||||
if mode.IsVisualMode() && posInsideSelection(w, mode, col, lineNumber) {
|
if mode.IsVisualMode() && posInsideSelection(w, mode, col, lineNumber) {
|
||||||
vis := styles.VisualHighlightWithTextColor(s)
|
vis := t.VisualHighlightWithTextColor(s)
|
||||||
view.WriteString(vis.Render(string(runes[col])))
|
view.WriteString(vis.Render(string(runes[col])))
|
||||||
} else {
|
} else {
|
||||||
view.WriteString(s.Render(string(runes[col])))
|
view.WriteString(s.Render(string(runes[col])))
|
||||||
}
|
}
|
||||||
// Allow highlight on blank lines or chars
|
// Allow highlight on blank lines or chars
|
||||||
} else if mode.IsVisualMode() && posInsideSelection(w, mode, col, lineNumber) {
|
} else if mode.IsVisualMode() && posInsideSelection(w, mode, col, lineNumber) {
|
||||||
view.WriteString(styles.VisualHighlight.Render(" "))
|
view.WriteString(t.VisualHightlight.Render(" "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pad remainder of line to window width with background color
|
// Pad remainder of line to window width with background color
|
||||||
dif := w.Width - lipgloss.Width(view.String())
|
dif := w.Width - lipgloss.Width(view.String())
|
||||||
if dif > 0 {
|
if dif > 0 {
|
||||||
view.WriteString(strings.Repeat(styles.BackgroundStyle.Render(" "), dif))
|
view.WriteString(strings.Repeat(t.Background.Render(" "), dif))
|
||||||
}
|
}
|
||||||
|
|
||||||
return view.String()
|
return view.String()
|
||||||
@ -127,7 +127,7 @@ func drawLine(w *core.Window, styles style.Styles, options core.WinOptions, mode
|
|||||||
|
|
||||||
// drawGutter: Renders the line number gutter with support for both absolute and
|
// drawGutter: Renders the line number gutter with support for both absolute and
|
||||||
// relative line numbers, highlighting the current line differently.
|
// relative line numbers, highlighting the current line differently.
|
||||||
func drawGutter(w *core.Window, styles style.Styles, options core.WinOptions, curLine int) string {
|
func drawGutter(w *core.Window, t theme.EditorTheme, options core.WinOptions, curLine int) string {
|
||||||
if !(options.Number || options.RelativeNumber) {
|
if !(options.Number || options.RelativeNumber) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@ -140,8 +140,8 @@ func drawGutter(w *core.Window, styles style.Styles, options core.WinOptions, cu
|
|||||||
lineNumber int
|
lineNumber int
|
||||||
|
|
||||||
gutter string
|
gutter string
|
||||||
gutterStyle = styles.Gutter
|
gutterStyle = t.Gutter.Default
|
||||||
gutterStyleCur = styles.GutterCurrentLine
|
gutterStyleCur = t.Gutter.CurrentLine
|
||||||
)
|
)
|
||||||
|
|
||||||
// If we have relative setting, set the numbers relatively
|
// If we have relative setting, set the numbers relatively
|
||||||
@ -173,9 +173,9 @@ func drawGutter(w *core.Window, styles style.Styles, options core.WinOptions, cu
|
|||||||
|
|
||||||
// drawStatusBar: Renders the status bar with mode and cursor position,
|
// drawStatusBar: Renders the status bar with mode and cursor position,
|
||||||
// padding the middle with spaces to fill the terminal width.
|
// padding the middle with spaces to fill the terminal width.
|
||||||
func drawStatusBar(w *core.Window, mode core.Mode, styles style.Styles) string {
|
func drawStatusBar(w *core.Window, mode core.Mode, t theme.EditorTheme) string {
|
||||||
left := leftBar(w, mode, styles)
|
left := leftBar(w, mode, t)
|
||||||
right := rightBar(w, mode, styles)
|
right := rightBar(w, mode, t)
|
||||||
|
|
||||||
diff := w.Width - (lipgloss.Width(left) + lipgloss.Width(right))
|
diff := w.Width - (lipgloss.Width(left) + lipgloss.Width(right))
|
||||||
|
|
||||||
@ -184,12 +184,12 @@ func drawStatusBar(w *core.Window, mode core.Mode, styles style.Styles) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
middle := strings.Repeat(styles.BackgroundStyle.Render(" "), diff)
|
middle := strings.Repeat(t.Background.Render(" "), diff)
|
||||||
return left + middle + right
|
return left + middle + right
|
||||||
}
|
}
|
||||||
|
|
||||||
// leftBar: Returns the left side of the status bar showing the current mode.
|
// leftBar: Returns the left side of the status bar showing the current mode.
|
||||||
func leftBar(w *core.Window, mode core.Mode, styles style.Styles) string {
|
func leftBar(w *core.Window, mode core.Mode, t theme.EditorTheme) string {
|
||||||
buf := w.Buffer
|
buf := w.Buffer
|
||||||
|
|
||||||
var flags []string
|
var flags []string
|
||||||
@ -206,12 +206,12 @@ func leftBar(w *core.Window, mode core.Mode, styles style.Styles) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bar := fmt.Sprintf(" %s %s %s", mode.ToString(), buf.Filename, flagStr)
|
bar := fmt.Sprintf(" %s %s %s", mode.ToString(), buf.Filename, flagStr)
|
||||||
return styles.LineStyle.Render(bar)
|
return t.Line.Render(bar)
|
||||||
}
|
}
|
||||||
|
|
||||||
// rightBar: Returns the right side of the status bar showing cursor position
|
// rightBar: Returns the right side of the status bar showing cursor position
|
||||||
// and selection count in visual mode.
|
// and selection count in visual mode.
|
||||||
func rightBar(w *core.Window, mode core.Mode, styles style.Styles) (bar string) {
|
func rightBar(w *core.Window, mode core.Mode, t theme.EditorTheme) (bar string) {
|
||||||
if mode.IsVisualMode() {
|
if mode.IsVisualMode() {
|
||||||
lineCount := max(w.Anchor.Line, w.Cursor.Line) - min(w.Anchor.Line, w.Cursor.Line) + 1
|
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)
|
bar = fmt.Sprintf("%d:%d <%d>", w.Cursor.Line+1, w.Cursor.Col+1, lineCount)
|
||||||
@ -219,44 +219,43 @@ func rightBar(w *core.Window, mode core.Mode, styles style.Styles) (bar string)
|
|||||||
bar = fmt.Sprintf("%d:%d ", w.Cursor.Line+1, w.Cursor.Col+1)
|
bar = fmt.Sprintf("%d:%d ", w.Cursor.Line+1, w.Cursor.Col+1)
|
||||||
}
|
}
|
||||||
buf := w.Buffer
|
buf := w.Buffer
|
||||||
bar = styles.LineStyle.Render(fmt.Sprintf("%s %s", buf.Filetype, bar))
|
bar = t.Line.Render(fmt.Sprintf("%s %s", buf.Filetype, bar))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawCommandBar: Renders the command line showing command input, errors, or
|
// drawCommandBar: Renders the command line showing command input, errors, or
|
||||||
// output depending on the current mode and state.
|
// output depending on the current mode and state.
|
||||||
func drawCommandBar(m Model) string {
|
func drawCommandBar(m Model, t theme.EditorTheme) string {
|
||||||
styles := m.Styles()
|
|
||||||
|
|
||||||
// Compute left bar (command side)
|
// Compute left bar (command side)
|
||||||
var leftBar string
|
var leftBar string
|
||||||
if m.Mode() == core.CommandMode {
|
if m.Mode() == core.CommandMode {
|
||||||
leftBar = styles.LineStyle.Render(":")
|
leftBar = t.Line.Render(":")
|
||||||
cmd := []rune(m.Command())
|
cmd := []rune(m.Command())
|
||||||
cur := m.CommandCursor()
|
cur := m.CommandCursor()
|
||||||
for i, r := range cmd {
|
for i, r := range cmd {
|
||||||
if i == cur {
|
if i == cur {
|
||||||
leftBar += styles.DefaultCursorStyle(m.Mode()).Render(string(r))
|
leftBar += t.DefaultCursor(m.Mode()).Render(string(r))
|
||||||
} else {
|
} else {
|
||||||
leftBar += styles.LineStyle.Render(string(r))
|
leftBar += t.Line.Render(string(r))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Cursor at end of command
|
// Cursor at end of command
|
||||||
if cur >= len(cmd) {
|
if cur >= len(cmd) {
|
||||||
leftBar += styles.DefaultCursorStyle(m.Mode()).Render(" ")
|
leftBar += t.DefaultCursor(m.Mode()).Render(" ")
|
||||||
}
|
}
|
||||||
// bar = fmt.Sprintf("%s %d", bar, cur)
|
// bar = fmt.Sprintf("%s %d", bar, cur)
|
||||||
} else if out := m.CommandOutput(); out != nil && len(out.Lines) > 0 && out.Inline {
|
} else if out := m.CommandOutput(); out != nil && len(out.Lines) > 0 && out.Inline {
|
||||||
// TODO: This is not perfect, temporary
|
// TODO: This is not perfect, temporary
|
||||||
text := strings.Join(out.Lines, " ")
|
text := strings.Join(out.Lines, " ")
|
||||||
if out.IsError {
|
if out.IsError {
|
||||||
leftBar = styles.CommandError.Render(text)
|
leftBar = t.CommandLine.Error.Render(text)
|
||||||
} else {
|
} else {
|
||||||
leftBar = styles.LineStyle.Render(text)
|
leftBar = t.Line.Render(text)
|
||||||
}
|
}
|
||||||
} else if strings.TrimSpace(m.Command()) != "" {
|
} else if strings.TrimSpace(m.Command()) != "" {
|
||||||
content := fmt.Sprintf(":%s", m.Command())
|
content := fmt.Sprintf(":%s", m.Command())
|
||||||
leftBar = styles.LineStyle.Render(content) //
|
leftBar = t.Line.Render(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute right bar
|
// Compute right bar
|
||||||
@ -265,12 +264,12 @@ func drawCommandBar(m Model) string {
|
|||||||
if len(m.input.Pending()) > 0 {
|
if len(m.input.Pending()) > 0 {
|
||||||
width := 10 // Size of the block to display
|
width := 10 // Size of the block to display
|
||||||
content := fmt.Sprintf("%-*s", width, m.input.Pending())
|
content := fmt.Sprintf("%-*s", width, m.input.Pending())
|
||||||
rightBar = styles.LineStyle.Render(content)
|
rightBar = t.Line.Render(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
dif := m.termWidth - (lipgloss.Width(leftBar) + lipgloss.Width(rightBar))
|
dif := m.termWidth - (lipgloss.Width(leftBar) + lipgloss.Width(rightBar))
|
||||||
|
|
||||||
bar := leftBar + strings.Repeat(styles.BackgroundStyle.Render(" "), max(0, dif)) + rightBar
|
bar := leftBar + strings.Repeat(t.Background.Render(" "), max(0, dif)) + rightBar
|
||||||
return bar
|
return bar
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,7 +320,7 @@ func posInsideSelection(w *core.Window, mode core.Mode, col, line int) bool {
|
|||||||
|
|
||||||
// overlayCommandOutputWindow: Draw the overlay of the command output window. This will override
|
// overlayCommandOutputWindow: Draw the overlay of the command output window. This will override
|
||||||
// (overlay) the displayed content, so it should be used only when needed.
|
// (overlay) the displayed content, so it should be used only when needed.
|
||||||
func overlayCommandOutputWindow(view string, cmd *core.CommandOutput, styles style.Styles, termWidth int, termHeight int) string {
|
func overlayCommandOutputWindow(view string, cmd *core.CommandOutput, t theme.EditorTheme, termWidth int, termHeight int) string {
|
||||||
// Safety check
|
// Safety check
|
||||||
if cmd == nil {
|
if cmd == nil {
|
||||||
return view
|
return view
|
||||||
@ -332,22 +331,22 @@ func overlayCommandOutputWindow(view string, cmd *core.CommandOutput, styles sty
|
|||||||
|
|
||||||
// Build the overlay
|
// Build the overlay
|
||||||
var overlay []string
|
var overlay []string
|
||||||
overlay = append(overlay, styles.CommandOutputBorder.Render(strings.Repeat(" ", termWidth)))
|
overlay = append(overlay, t.CommandLine.OutputBorder.Render(strings.Repeat(" ", termWidth)))
|
||||||
|
|
||||||
if strings.TrimSpace(cmd.Title) != "" {
|
if strings.TrimSpace(cmd.Title) != "" {
|
||||||
title := styles.LineStyle.Render(cmd.Title)
|
title := t.Line.Render(cmd.Title)
|
||||||
overlay = append(overlay, title)
|
overlay = append(overlay, title)
|
||||||
}
|
}
|
||||||
viewLines := cmd.Viewport(termHeight)
|
viewLines := cmd.Viewport(termHeight)
|
||||||
for _, l := range viewLines {
|
for _, l := range viewLines {
|
||||||
content := styles.LineStyle.Render(strings.ReplaceAll(l, "\n", "\\n"))
|
content := t.Line.Render(strings.ReplaceAll(l, "\n", "\\n"))
|
||||||
overlay = append(overlay, content)
|
overlay = append(overlay, content)
|
||||||
}
|
}
|
||||||
msg := core.CommandOutputExitMessage
|
msg := core.CommandOutputExitMessage
|
||||||
if len(cmd.Lines) > len(cmd.Viewport(termHeight)) {
|
if len(cmd.Lines) > len(cmd.Viewport(termHeight)) {
|
||||||
msg += ". " + core.CommandOutputScrollMessage
|
msg += ". " + core.CommandOutputScrollMessage
|
||||||
}
|
}
|
||||||
overlay = append(overlay, styles.CommandContinueMessage.Render(msg))
|
overlay = append(overlay, t.CommandLine.ContinueMessage.Render(msg))
|
||||||
|
|
||||||
// NOTE: strings.Split on "\n" is safe as long as no style uses .Width()/.Height()/.Padding()/.Margin(),
|
// NOTE: strings.Split on "\n" is safe as long as no style uses .Width()/.Height()/.Padding()/.Margin(),
|
||||||
// which would cause Lipgloss to embed newlines internally and corrupt the line count.
|
// which would cause Lipgloss to embed newlines internally and corrupt the line count.
|
||||||
@ -356,7 +355,7 @@ func overlayCommandOutputWindow(view string, cmd *core.CommandOutput, styles sty
|
|||||||
// Add background color to end of each line
|
// Add background color to end of each line
|
||||||
for i, l := range overlay {
|
for i, l := range overlay {
|
||||||
dif := termWidth - lipgloss.Width(l)
|
dif := termWidth - lipgloss.Width(l)
|
||||||
overlay[i] += styles.BackgroundStyle.Render(strings.Repeat(" ", dif))
|
overlay[i] += t.Background.Render(strings.Repeat(" ", dif))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove 'h' lines from back of view and append overlay
|
// Remove 'h' lines from back of view and append overlay
|
||||||
|
|||||||
@ -1,52 +0,0 @@
|
|||||||
package style
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CaptureStyle(base lipgloss.Style, capture string) lipgloss.Style {
|
|
||||||
full := strings.ToLower(strings.TrimSpace(capture))
|
|
||||||
baseName := strings.Split(full, ".")[0]
|
|
||||||
|
|
||||||
switch full {
|
|
||||||
case "keyword", "keyword.type", "keyword.function", "keyword.coroutine", "keyword.repeat", "keyword.import", "keyword.conditional":
|
|
||||||
return base.Foreground(lipgloss.Color("#c678dd"))
|
|
||||||
case "function", "function.call", "function.method", "function.method.call":
|
|
||||||
return base.Foreground(lipgloss.Color("#61afef"))
|
|
||||||
case "function.builtin", "constructor", "keyword.return":
|
|
||||||
return base.Foreground(lipgloss.Color("#ff5f5f"))
|
|
||||||
case "type", "type.builtin", "type.definition":
|
|
||||||
return base.Foreground(lipgloss.Color("#e5c07b"))
|
|
||||||
case "string", "string.escape":
|
|
||||||
return base.Foreground(lipgloss.Color("#98c379"))
|
|
||||||
case "number", "number.float", "boolean", "constant", "constant.builtin":
|
|
||||||
return base.Foreground(lipgloss.Color("#56b6c2"))
|
|
||||||
case "operator", "punctuation.delimiter", "punctuation.bracket":
|
|
||||||
return base.Foreground(lipgloss.Color("#d19a66"))
|
|
||||||
case "comment", "comment.documentation":
|
|
||||||
return base.Foreground(lipgloss.Color("#7f848e"))
|
|
||||||
case "variable.parameter":
|
|
||||||
return base.Foreground(lipgloss.Color("#dcdfe4")).Italic(true)
|
|
||||||
case "module", "label", "property", "variable.member", "variable":
|
|
||||||
return base.Foreground(lipgloss.Color("#dcdfe4"))
|
|
||||||
}
|
|
||||||
|
|
||||||
switch baseName {
|
|
||||||
case "keyword":
|
|
||||||
return base.Foreground(lipgloss.Color("#c678dd"))
|
|
||||||
case "function":
|
|
||||||
return base.Foreground(lipgloss.Color("#61afef"))
|
|
||||||
case "type":
|
|
||||||
return base.Foreground(lipgloss.Color("#e5c07b"))
|
|
||||||
case "string":
|
|
||||||
return base.Foreground(lipgloss.Color("#98c379"))
|
|
||||||
case "number", "boolean", "constant":
|
|
||||||
return base.Foreground(lipgloss.Color("#56b6c2"))
|
|
||||||
case "comment":
|
|
||||||
return base.Foreground(lipgloss.Color("#7f848e"))
|
|
||||||
default:
|
|
||||||
return base
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,141 +0,0 @@
|
|||||||
package style
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Styles holds all the visual styling for the editor.
|
|
||||||
type Styles struct {
|
|
||||||
// Cursor styles by mode
|
|
||||||
CursorNormal lipgloss.Style
|
|
||||||
CursorInsert lipgloss.Style
|
|
||||||
CursorCommand lipgloss.Style
|
|
||||||
CursorReplace lipgloss.Style
|
|
||||||
|
|
||||||
// Gutter (line numbers)
|
|
||||||
Gutter lipgloss.Style
|
|
||||||
GutterCurrentLine lipgloss.Style
|
|
||||||
|
|
||||||
// Visual mode
|
|
||||||
VisualHighlight lipgloss.Style
|
|
||||||
VisualAnchor lipgloss.Style
|
|
||||||
|
|
||||||
// Status bar
|
|
||||||
StatusBar lipgloss.Style
|
|
||||||
StatusBarActive lipgloss.Style
|
|
||||||
|
|
||||||
// Command line
|
|
||||||
CommandError lipgloss.Style
|
|
||||||
CommandOutputBorder lipgloss.Style
|
|
||||||
CommandContinueMessage lipgloss.Style
|
|
||||||
|
|
||||||
// General styles
|
|
||||||
LineStyle lipgloss.Style
|
|
||||||
BackgroundStyle lipgloss.Style
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultStyles: Returns the default editor color scheme.
|
|
||||||
func DefaultStyles() Styles {
|
|
||||||
bg := lipgloss.Color("#1f2335")
|
|
||||||
fg := lipgloss.Color("#dcd7ba")
|
|
||||||
|
|
||||||
return Styles{
|
|
||||||
CursorNormal: lipgloss.NewStyle().
|
|
||||||
Background(fg).
|
|
||||||
Foreground(bg),
|
|
||||||
|
|
||||||
CursorInsert: lipgloss.NewStyle().
|
|
||||||
Background(bg).
|
|
||||||
Foreground(fg).
|
|
||||||
Underline(true),
|
|
||||||
|
|
||||||
CursorCommand: lipgloss.NewStyle().
|
|
||||||
Background(fg).
|
|
||||||
Foreground(bg),
|
|
||||||
|
|
||||||
CursorReplace: lipgloss.NewStyle().
|
|
||||||
Background(bg).
|
|
||||||
Foreground(fg).
|
|
||||||
Underline(true),
|
|
||||||
|
|
||||||
Gutter: lipgloss.NewStyle().
|
|
||||||
Background(lipgloss.Color("#181b2a")).
|
|
||||||
Foreground(lipgloss.Color("#7e8399")),
|
|
||||||
|
|
||||||
GutterCurrentLine: lipgloss.NewStyle().
|
|
||||||
Background(lipgloss.Color("#181b2a")).
|
|
||||||
Foreground(lipgloss.Color("#e6c384")),
|
|
||||||
|
|
||||||
VisualHighlight: lipgloss.NewStyle().
|
|
||||||
Background(lipgloss.Color("#2f334d")),
|
|
||||||
|
|
||||||
VisualAnchor: lipgloss.NewStyle().
|
|
||||||
Background(lipgloss.Color("#3a3f5f")),
|
|
||||||
|
|
||||||
StatusBar: lipgloss.NewStyle().
|
|
||||||
Background(lipgloss.Color("#181b2a")).
|
|
||||||
Foreground(lipgloss.Color("#8ea4a2")),
|
|
||||||
|
|
||||||
StatusBarActive: lipgloss.NewStyle().
|
|
||||||
Background(lipgloss.Color("#223249")).
|
|
||||||
Foreground(lipgloss.Color("#9ec1cf")),
|
|
||||||
|
|
||||||
CommandError: lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#e82424")),
|
|
||||||
|
|
||||||
CommandOutputBorder: lipgloss.NewStyle().
|
|
||||||
Background(lipgloss.Color("#11131d")),
|
|
||||||
|
|
||||||
CommandContinueMessage: lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("#7aa2f7")),
|
|
||||||
|
|
||||||
LineStyle: lipgloss.NewStyle().
|
|
||||||
Foreground(fg).
|
|
||||||
Background(bg),
|
|
||||||
|
|
||||||
BackgroundStyle: lipgloss.NewStyle().
|
|
||||||
Background(bg),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Styles.DefaultCursorStyle: Returns the appropriate cursor style for the given mode.
|
|
||||||
func (s Styles) DefaultCursorStyle(mode core.Mode) lipgloss.Style {
|
|
||||||
switch mode {
|
|
||||||
case core.InsertMode:
|
|
||||||
return s.CursorInsert
|
|
||||||
case core.CommandMode:
|
|
||||||
return s.CursorCommand
|
|
||||||
case core.ReplaceMode:
|
|
||||||
return s.CursorReplace
|
|
||||||
default:
|
|
||||||
return s.CursorNormal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Styles.CursorStyle: Returns a cursor style derived from the text style.
|
|
||||||
func (s Styles) CursorStyle(mode core.Mode, textStyle lipgloss.Style) lipgloss.Style {
|
|
||||||
switch mode {
|
|
||||||
case core.NormalMode, core.VisualLineMode, core.VisualBlockMode, core.VisualMode:
|
|
||||||
return lipgloss.NewStyle().
|
|
||||||
Background(textStyle.GetForeground()).
|
|
||||||
Foreground(textStyle.GetBackground())
|
|
||||||
case core.ReplaceMode, core.WaitingMode:
|
|
||||||
return lipgloss.NewStyle().
|
|
||||||
Background(textStyle.GetBackground()).
|
|
||||||
Foreground(textStyle.GetForeground()).
|
|
||||||
Underline(true)
|
|
||||||
default:
|
|
||||||
return lipgloss.NewStyle().
|
|
||||||
Background(s.BackgroundStyle.GetBackground()).
|
|
||||||
Foreground(textStyle.GetForeground()).
|
|
||||||
Underline(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Styles.VisualHighlightWithTextColor: Applies visual background while preserving text color.
|
|
||||||
func (s Styles) VisualHighlightWithTextColor(textStyle lipgloss.Style) lipgloss.Style {
|
|
||||||
return lipgloss.NewStyle().
|
|
||||||
Background(s.VisualHighlight.GetBackground()).
|
|
||||||
Foreground(textStyle.GetForeground())
|
|
||||||
}
|
|
||||||
@ -95,6 +95,10 @@
|
|||||||
(attribute_value) @string.special.url))
|
(attribute_value) @string.special.url))
|
||||||
(#any-of? @_attr "href" "src"))
|
(#any-of? @_attr "href" "src"))
|
||||||
|
|
||||||
|
((attribute
|
||||||
|
(attribute_name) @tag.attribute.url)
|
||||||
|
(#any-of? @tag.attribute.url "href" "src"))
|
||||||
|
|
||||||
[
|
[
|
||||||
"<"
|
"<"
|
||||||
">"
|
">"
|
||||||
@ -103,3 +107,9 @@
|
|||||||
] @tag.delimiter
|
] @tag.delimiter
|
||||||
|
|
||||||
"=" @operator
|
"=" @operator
|
||||||
|
|
||||||
|
(doctype) @constant
|
||||||
|
|
||||||
|
"<!" @tag.delimiter
|
||||||
|
|
||||||
|
(entity) @character.special
|
||||||
|
|||||||
@ -3,21 +3,34 @@ package syntax
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/style"
|
"git.gophernest.net/azpect/TextEditor/internal/theme"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
sitter "github.com/tree-sitter/go-tree-sitter"
|
sitter "github.com/tree-sitter/go-tree-sitter"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TreeSitterEngine provides syntax highlighting using Tree-sitter queries.
|
||||||
|
//
|
||||||
|
// The engine stores per-buffer parser state and a cached style map so redraws
|
||||||
|
// can reuse prior work. It supports both full rebuilds and incremental edits:
|
||||||
|
// - full rebuilds when a buffer is first seen, language changes, or state is invalid
|
||||||
|
// - incremental updates when ApplyEdit provides enough information to reparse
|
||||||
|
// only changed regions
|
||||||
|
//
|
||||||
|
// Cached styles are represented as one style per rune for each line.
|
||||||
type TreeSitterEngine struct {
|
type TreeSitterEngine struct {
|
||||||
styles style.Styles
|
editorTheme theme.EditorTheme
|
||||||
registry *languageRegistry
|
registry *languageRegistry
|
||||||
|
|
||||||
cache map[*core.Buffer]*bufferCache
|
cache map[*core.Buffer]*bufferCache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bufferCache stores all derived highlighting state for a single buffer.
|
||||||
|
//
|
||||||
|
// It contains both style output (`lines`) and parse/query state (`parser`,
|
||||||
|
// `tree`, `source`, language/query bindings) so the engine can incrementally
|
||||||
|
// update only dirty lines instead of recomputing the whole file each frame.
|
||||||
type bufferCache struct {
|
type bufferCache struct {
|
||||||
built bool
|
built bool
|
||||||
lines map[int][]lipgloss.Style
|
lines map[int][]lipgloss.Style
|
||||||
@ -34,11 +47,19 @@ type bufferCache struct {
|
|||||||
query *sitter.Query
|
query *sitter.Query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lineRange is an inclusive line interval [start, end].
|
||||||
|
//
|
||||||
|
// Dirty tracking and partial restyling use this type to represent which rows
|
||||||
|
// need work.
|
||||||
type lineRange struct {
|
type lineRange struct {
|
||||||
start int
|
start int
|
||||||
end int
|
end int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// captureRange describes one Tree-sitter capture span.
|
||||||
|
//
|
||||||
|
// Coordinates are in row/byte-column space, matching Tree-sitter node
|
||||||
|
// positions. The range is later converted to rune indexes for style writes.
|
||||||
type captureRange struct {
|
type captureRange struct {
|
||||||
startRow uint
|
startRow uint
|
||||||
startCol uint
|
startCol uint
|
||||||
@ -47,19 +68,26 @@ type captureRange struct {
|
|||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTreeSitterEngine: Creates a new tree sitter engine with the styles
|
// NewTreeSitterEngine constructs a TreeSitterEngine with the provided style set.
|
||||||
// provided attached.
|
|
||||||
//
|
//
|
||||||
// Currently, this engine only support GoLang. But more languages can be
|
// Language support is resolved through the language registry, so the engine can
|
||||||
// added with easy.
|
// work with any language/query pair registered there.
|
||||||
func NewTreeSitterEngine(styles style.Styles) *TreeSitterEngine {
|
func NewTreeSitterEngine(t theme.EditorTheme) *TreeSitterEngine {
|
||||||
return &TreeSitterEngine{
|
return &TreeSitterEngine{
|
||||||
styles: styles,
|
editorTheme: t,
|
||||||
registry: newLanguageRegistry(),
|
registry: newLanguageRegistry(),
|
||||||
cache: map[*core.Buffer]*bufferCache{},
|
cache: map[*core.Buffer]*bufferCache{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrepareBuffer ensures highlighting data for buf is ready to read.
|
||||||
|
//
|
||||||
|
// This method is idempotent: if cached styles are already valid (`built`), it
|
||||||
|
// returns immediately. Otherwise it resolves language support and performs a
|
||||||
|
// rebuild pass (full or dirty-range-based) to refresh `bc.lines`.
|
||||||
|
//
|
||||||
|
// If the buffer language is unsupported or resolution fails, it still marks the
|
||||||
|
// cache as built with an empty style map so callers can safely continue.
|
||||||
func (e *TreeSitterEngine) PrepareBuffer(buf *core.Buffer) {
|
func (e *TreeSitterEngine) PrepareBuffer(buf *core.Buffer) {
|
||||||
// Cannot prepare a nil buffer
|
// Cannot prepare a nil buffer
|
||||||
if buf == nil {
|
if buf == nil {
|
||||||
@ -90,6 +118,11 @@ func (e *TreeSitterEngine) PrepareBuffer(buf *core.Buffer) {
|
|||||||
e.buildFullBuffer(buf, bc)
|
e.buildFullBuffer(buf, bc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LineStyleMap returns the style row for a specific line in buf.
|
||||||
|
//
|
||||||
|
// It first guarantees buffer preparation, then returns cached styles when
|
||||||
|
// available. Missing lines are lazily initialized to the base line style and
|
||||||
|
// stored in cache to keep downstream rendering logic simple.
|
||||||
func (e *TreeSitterEngine) LineStyleMap(buf *core.Buffer, line int) []lipgloss.Style {
|
func (e *TreeSitterEngine) LineStyleMap(buf *core.Buffer, line int) []lipgloss.Style {
|
||||||
if buf == nil {
|
if buf == nil {
|
||||||
return nil
|
return nil
|
||||||
@ -105,12 +138,23 @@ func (e *TreeSitterEngine) LineStyleMap(buf *core.Buffer, line int) []lipgloss.S
|
|||||||
runes := []rune(buf.Line(line))
|
runes := []rune(buf.Line(line))
|
||||||
out := make([]lipgloss.Style, len(runes))
|
out := make([]lipgloss.Style, len(runes))
|
||||||
for i := range out {
|
for i := range out {
|
||||||
out[i] = e.styles.LineStyle
|
out[i] = e.editorTheme.Line
|
||||||
}
|
}
|
||||||
bc.lines[line] = out
|
bc.lines[line] = out
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ApplyEdit applies an incremental buffer edit to parser and style cache state.
|
||||||
|
//
|
||||||
|
// Workflow:
|
||||||
|
// - validate buffer and language support
|
||||||
|
// - apply the edit to the current parse tree (InputEdit)
|
||||||
|
// - reparse using the previous tree as incremental context
|
||||||
|
// - collect changed line ranges from both the user edit and parser changes
|
||||||
|
// - mark cache as unbuilt so the next PrepareBuffer restyles only dirty areas
|
||||||
|
//
|
||||||
|
// If incremental parsing cannot proceed (missing parser/tree/source or parse
|
||||||
|
// failure), it falls back to a full-dirty rebuild on the next preparation.
|
||||||
func (e *TreeSitterEngine) ApplyEdit(buf *core.Buffer, edit *core.BufferEdit) {
|
func (e *TreeSitterEngine) ApplyEdit(buf *core.Buffer, edit *core.BufferEdit) {
|
||||||
if buf == nil || edit == nil {
|
if buf == nil || edit == nil {
|
||||||
return
|
return
|
||||||
@ -172,8 +216,9 @@ func (e *TreeSitterEngine) ApplyEdit(buf *core.Buffer, edit *core.BufferEdit) {
|
|||||||
bc.built = false
|
bc.built = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// TreeSitterEngine.InvalidateBuffer: Deletes the entire buffers cache from the engine. If the
|
// InvalidateBuffer marks all cached highlighting data for buf as stale.
|
||||||
// buffer provided is nil, this function does nothing.
|
//
|
||||||
|
// The next PrepareBuffer call will rebuild styles from scratch for the buffer.
|
||||||
func (e *TreeSitterEngine) InvalidateBuffer(buf *core.Buffer) {
|
func (e *TreeSitterEngine) InvalidateBuffer(buf *core.Buffer) {
|
||||||
if buf == nil {
|
if buf == nil {
|
||||||
return
|
return
|
||||||
@ -184,9 +229,11 @@ func (e *TreeSitterEngine) InvalidateBuffer(buf *core.Buffer) {
|
|||||||
bc.dirty = nil
|
bc.dirty = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TreeSitterEngine.InvalidateLines: Deletes lines between start and end (inclusive) from the
|
// InvalidateLines marks a line interval in buf as dirty.
|
||||||
// buffers cache. Then marks the cache as "unbuilt." If the buffer provided is nil, this function
|
//
|
||||||
// does nothing.
|
// The range is inclusive and normalized by addDirtyRange. On the next
|
||||||
|
// preparation pass, those lines (plus capture-context neighbors) are
|
||||||
|
// recalculated while unchanged lines are preserved.
|
||||||
func (e *TreeSitterEngine) InvalidateLines(buf *core.Buffer, startLine, endLine int) {
|
func (e *TreeSitterEngine) InvalidateLines(buf *core.Buffer, startLine, endLine int) {
|
||||||
if buf == nil {
|
if buf == nil {
|
||||||
return
|
return
|
||||||
@ -196,8 +243,14 @@ func (e *TreeSitterEngine) InvalidateLines(buf *core.Buffer, startLine, endLine
|
|||||||
bc.built = false
|
bc.built = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// TreeSitterEngine.supportsBuffer: Returns whether the buffer can be parsed and highlighted
|
// resolveBufferLanguage resolves and applies language/query config for buf.
|
||||||
// by the engine. When false, there should be a fallback.
|
//
|
||||||
|
// It asks the registry to resolve filetype/filename to a concrete language id,
|
||||||
|
// language object, and highlight query. When the resolved language id changes,
|
||||||
|
// parser/query bindings are updated and the cache is marked dirty for rebuild.
|
||||||
|
//
|
||||||
|
// Returns (resolved, true, nil) on success. When unsupported it returns
|
||||||
|
// (nil, false, nil). Resolution errors are returned as the third value.
|
||||||
func (e *TreeSitterEngine) resolveBufferLanguage(buf *core.Buffer, bc *bufferCache) (*resolvedLanguage, bool, error) {
|
func (e *TreeSitterEngine) resolveBufferLanguage(buf *core.Buffer, bc *bufferCache) (*resolvedLanguage, bool, error) {
|
||||||
if e.registry == nil {
|
if e.registry == nil {
|
||||||
e.registry = newLanguageRegistry()
|
e.registry = newLanguageRegistry()
|
||||||
@ -222,8 +275,10 @@ func (e *TreeSitterEngine) resolveBufferLanguage(buf *core.Buffer, bc *bufferCac
|
|||||||
return resolved, true, nil
|
return resolved, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TreeSitterEngine.getCache: Returns the buffers cache. If the cache does not exist, a new one
|
// getCache returns the cache object associated with buf, creating it if needed.
|
||||||
// is created and applied to the engines cache map.
|
//
|
||||||
|
// New caches start with an initialized lines map and default zero-values for
|
||||||
|
// parse/highlight state.
|
||||||
func (e *TreeSitterEngine) getCache(buf *core.Buffer) *bufferCache {
|
func (e *TreeSitterEngine) getCache(buf *core.Buffer) *bufferCache {
|
||||||
if bc, ok := e.cache[buf]; ok {
|
if bc, ok := e.cache[buf]; ok {
|
||||||
return bc
|
return bc
|
||||||
@ -233,6 +288,15 @@ func (e *TreeSitterEngine) getCache(buf *core.Buffer) *bufferCache {
|
|||||||
return bc
|
return bc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildFullBuffer rebuilds highlight styles for buf using current cache state.
|
||||||
|
//
|
||||||
|
// Despite the name, this method handles both full and partial updates:
|
||||||
|
// - full rebuild: reset every line to base style, query entire file
|
||||||
|
// - partial rebuild: reset only dirty lines, query around dirty ranges
|
||||||
|
//
|
||||||
|
// It (re)parses source when needed, collects query captures, sorts captures by
|
||||||
|
// precedence order, then writes styles onto per-rune line slices. After a
|
||||||
|
// successful pass it clears dirty flags and marks the cache as built.
|
||||||
func (e *TreeSitterEngine) buildFullBuffer(buf *core.Buffer, bc *bufferCache) {
|
func (e *TreeSitterEngine) buildFullBuffer(buf *core.Buffer, bc *bufferCache) {
|
||||||
lineCount := buf.LineCount()
|
lineCount := buf.LineCount()
|
||||||
|
|
||||||
@ -248,13 +312,13 @@ func (e *TreeSitterEngine) buildFullBuffer(buf *core.Buffer, bc *bufferCache) {
|
|||||||
if fullRebuild {
|
if fullRebuild {
|
||||||
bc.lines = map[int][]lipgloss.Style{}
|
bc.lines = map[int][]lipgloss.Style{}
|
||||||
for i := range lineCount {
|
for i := range lineCount {
|
||||||
bc.lines[i] = defaultLineStyles(lines[i], e.styles.LineStyle)
|
bc.lines[i] = defaultLineStyles(lines[i], e.editorTheme.Line)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dirty := normalizedDirtyRanges(bc.dirty, lineCount)
|
dirty := normalizedDirtyRanges(bc.dirty, lineCount)
|
||||||
for _, r := range dirty {
|
for _, r := range dirty {
|
||||||
for i := r.start; i <= r.end; i++ {
|
for i := r.start; i <= r.end; i++ {
|
||||||
bc.lines[i] = defaultLineStyles(lines[i], e.styles.LineStyle)
|
bc.lines[i] = defaultLineStyles(lines[i], e.editorTheme.Line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -333,7 +397,7 @@ func (e *TreeSitterEngine) buildFullBuffer(buf *core.Buffer, bc *bufferCache) {
|
|||||||
// rewrites.
|
// rewrites.
|
||||||
targetDirty := normalizedDirtyRanges(bc.dirty, lineCount)
|
targetDirty := normalizedDirtyRanges(bc.dirty, lineCount)
|
||||||
for _, c := range captures {
|
for _, c := range captures {
|
||||||
sty := style.CaptureStyle(e.styles.LineStyle, c.name)
|
sty := e.editorTheme.CaptureStyle(c.name)
|
||||||
for row := c.startRow; row <= c.endRow; row++ {
|
for row := c.startRow; row <= c.endRow; row++ {
|
||||||
if int(row) >= len(lines) {
|
if int(row) >= len(lines) {
|
||||||
break
|
break
|
||||||
@ -378,143 +442,3 @@ func (e *TreeSitterEngine) buildFullBuffer(buf *core.Buffer, bc *bufferCache) {
|
|||||||
bc.count = lineCount
|
bc.count = lineCount
|
||||||
bc.built = true
|
bc.built = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func addDirtyRange(bc *bufferCache, start, end int) {
|
|
||||||
if bc == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if end < start {
|
|
||||||
start, end = end, start
|
|
||||||
}
|
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
if end < 0 {
|
|
||||||
end = 0
|
|
||||||
}
|
|
||||||
bc.dirty = append(bc.dirty, lineRange{start: start, end: end})
|
|
||||||
bc.dirty = mergeRanges(bc.dirty)
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizedDirtyRanges(ranges []lineRange, lineCount int) []lineRange {
|
|
||||||
if lineCount <= 0 || len(ranges) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
clamped := make([]lineRange, 0, len(ranges))
|
|
||||||
for _, r := range ranges {
|
|
||||||
start := max(0, r.start)
|
|
||||||
end := min(lineCount-1, r.end)
|
|
||||||
if start > end {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
clamped = append(clamped, lineRange{start: start, end: end})
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergeRanges(clamped)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeRanges(ranges []lineRange) []lineRange {
|
|
||||||
if len(ranges) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(ranges, func(i, j int) bool {
|
|
||||||
if ranges[i].start == ranges[j].start {
|
|
||||||
return ranges[i].end < ranges[j].end
|
|
||||||
}
|
|
||||||
return ranges[i].start < ranges[j].start
|
|
||||||
})
|
|
||||||
|
|
||||||
merged := make([]lineRange, 0, len(ranges))
|
|
||||||
cur := ranges[0]
|
|
||||||
for i := 1; i < len(ranges); i++ {
|
|
||||||
n := ranges[i]
|
|
||||||
if n.start <= cur.end+1 {
|
|
||||||
if n.end > cur.end {
|
|
||||||
cur.end = n.end
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
merged = append(merged, cur)
|
|
||||||
cur = n
|
|
||||||
}
|
|
||||||
merged = append(merged, cur)
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
func rowInRanges(row int, ranges []lineRange) bool {
|
|
||||||
for _, r := range ranges {
|
|
||||||
if row >= r.start && row <= r.end {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultLineStyles(line string, base lipgloss.Style) []lipgloss.Style {
|
|
||||||
runes := []rune(line)
|
|
||||||
row := make([]lipgloss.Style, len(runes))
|
|
||||||
for i := range row {
|
|
||||||
row[i] = base
|
|
||||||
}
|
|
||||||
return row
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectCaptures(iter sitter.QueryCaptures, query *sitter.Query) []captureRange {
|
|
||||||
if query == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
names := query.CaptureNames()
|
|
||||||
out := []captureRange{}
|
|
||||||
for match, captureIdx := iter.Next(); match != nil; match, captureIdx = iter.Next() {
|
|
||||||
capture := match.Captures[captureIdx]
|
|
||||||
if int(capture.Index) >= len(names) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name := names[capture.Index]
|
|
||||||
if name == "spell" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
node := capture.Node
|
|
||||||
start := node.StartPosition()
|
|
||||||
end := node.EndPosition()
|
|
||||||
out = append(out, captureRange{
|
|
||||||
startRow: start.Row,
|
|
||||||
startCol: start.Column,
|
|
||||||
endRow: end.Row,
|
|
||||||
endCol: end.Column,
|
|
||||||
name: name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildBufferSource(buf *core.Buffer) []byte {
|
|
||||||
lineCount := buf.LineCount()
|
|
||||||
if lineCount == 0 {
|
|
||||||
return []byte{}
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := make([]string, lineCount)
|
|
||||||
for i := range lineCount {
|
|
||||||
lines[i] = buf.Line(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
return []byte(strings.Join(lines, "\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func byteColToRuneIndex(line []byte, byteCol int) int {
|
|
||||||
if byteCol <= 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if byteCol >= len(line) {
|
|
||||||
return len([]rune(string(line)))
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := line[:byteCol]
|
|
||||||
return len([]rune(string(prefix)))
|
|
||||||
}
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/style"
|
"git.gophernest.net/azpect/TextEditor/internal/theme/themes"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -23,10 +23,10 @@ func TestTreeSitterEngineHighlightsGoKeywordAndString(t *testing.T) {
|
|||||||
Build()
|
Build()
|
||||||
buf := &b
|
buf := &b
|
||||||
|
|
||||||
engine := NewTreeSitterEngine(style.DefaultStyles())
|
engine := NewTreeSitterEngine(themes.NewDefaultTheme())
|
||||||
engine.PrepareBuffer(buf)
|
engine.PrepareBuffer(buf)
|
||||||
|
|
||||||
base := engine.styles.LineStyle
|
base := engine.editorTheme.Line
|
||||||
|
|
||||||
line0 := buf.Line(0)
|
line0 := buf.Line(0)
|
||||||
map0 := engine.LineStyleMap(buf, 0)
|
map0 := engine.LineStyleMap(buf, 0)
|
||||||
@ -63,10 +63,10 @@ func TestTreeSitterEngineHighlightsMultilineRawString(t *testing.T) {
|
|||||||
Build()
|
Build()
|
||||||
buf := &b
|
buf := &b
|
||||||
|
|
||||||
engine := NewTreeSitterEngine(style.DefaultStyles())
|
engine := NewTreeSitterEngine(themes.NewDefaultTheme())
|
||||||
engine.PrepareBuffer(buf)
|
engine.PrepareBuffer(buf)
|
||||||
|
|
||||||
base := engine.styles.LineStyle
|
base := engine.editorTheme.Line
|
||||||
map3 := engine.LineStyleMap(buf, 3)
|
map3 := engine.LineStyleMap(buf, 3)
|
||||||
if len(map3) == 0 {
|
if len(map3) == 0 {
|
||||||
t.Fatalf("expected style map on multiline raw string line")
|
t.Fatalf("expected style map on multiline raw string line")
|
||||||
@ -89,7 +89,7 @@ func TestTreeSitterEngineApplyEditUpdatesStyleCategory(t *testing.T) {
|
|||||||
Build()
|
Build()
|
||||||
buf := &b
|
buf := &b
|
||||||
|
|
||||||
engine := NewTreeSitterEngine(style.DefaultStyles())
|
engine := NewTreeSitterEngine(themes.NewDefaultTheme())
|
||||||
engine.PrepareBuffer(buf)
|
engine.PrepareBuffer(buf)
|
||||||
|
|
||||||
oldLine := buf.Line(2)
|
oldLine := buf.Line(2)
|
||||||
@ -121,7 +121,7 @@ func TestTreeSitterEngineApplyEditUpdatesStyleCategory(t *testing.T) {
|
|||||||
newMap := engine.LineStyleMap(buf, 2)
|
newMap := engine.LineStyleMap(buf, 2)
|
||||||
newStyle := newMap[newIdx]
|
newStyle := newMap[newIdx]
|
||||||
|
|
||||||
if styleEquivalent(newStyle, engine.styles.LineStyle) {
|
if styleEquivalent(newStyle, engine.editorTheme.Line) {
|
||||||
t.Fatalf("expected updated string to be highlighted")
|
t.Fatalf("expected updated string to be highlighted")
|
||||||
}
|
}
|
||||||
if styleEquivalent(oldStyle, newStyle) {
|
if styleEquivalent(oldStyle, newStyle) {
|
||||||
@ -137,7 +137,7 @@ func TestTreeSitterEngineApplyEditLineCountChangeForcesFullDirty(t *testing.T) {
|
|||||||
Build()
|
Build()
|
||||||
buf := &b
|
buf := &b
|
||||||
|
|
||||||
engine := NewTreeSitterEngine(style.DefaultStyles())
|
engine := NewTreeSitterEngine(themes.NewDefaultTheme())
|
||||||
engine.PrepareBuffer(buf)
|
engine.PrepareBuffer(buf)
|
||||||
bc := engine.getCache(buf)
|
bc := engine.getCache(buf)
|
||||||
|
|
||||||
@ -176,10 +176,10 @@ func TestTreeSitterEngineUnsupportedBufferFallsBackToDefaultStyles(t *testing.T)
|
|||||||
Build()
|
Build()
|
||||||
buf := &b
|
buf := &b
|
||||||
|
|
||||||
engine := NewTreeSitterEngine(style.DefaultStyles())
|
engine := NewTreeSitterEngine(themes.NewDefaultTheme())
|
||||||
engine.PrepareBuffer(buf)
|
engine.PrepareBuffer(buf)
|
||||||
|
|
||||||
base := engine.styles.LineStyle
|
base := engine.editorTheme.Line
|
||||||
line := buf.Line(0)
|
line := buf.Line(0)
|
||||||
m := engine.LineStyleMap(buf, 0)
|
m := engine.LineStyleMap(buf, 0)
|
||||||
if len(m) != len([]rune(line)) {
|
if len(m) != len([]rune(line)) {
|
||||||
@ -200,7 +200,7 @@ func TestTreeSitterEngineLastLineEditDoesNotPanicAndRebuilds(t *testing.T) {
|
|||||||
Build()
|
Build()
|
||||||
buf := &b
|
buf := &b
|
||||||
|
|
||||||
engine := NewTreeSitterEngine(style.DefaultStyles())
|
engine := NewTreeSitterEngine(themes.NewDefaultTheme())
|
||||||
engine.PrepareBuffer(buf)
|
engine.PrepareBuffer(buf)
|
||||||
bc := engine.getCache(buf)
|
bc := engine.getCache(buf)
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/style"
|
"git.gophernest.net/azpect/TextEditor/internal/theme/themes"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BenchmarkTreeSitterPrepareAndIncrementalEdit(b *testing.B) {
|
func BenchmarkTreeSitterPrepareAndIncrementalEdit(b *testing.B) {
|
||||||
@ -18,7 +18,7 @@ func BenchmarkTreeSitterPrepareAndIncrementalEdit(b *testing.B) {
|
|||||||
|
|
||||||
bld := core.NewBufferBuilder().WithFilename("bench.go").WithFiletype("go").WithLines(lines).Build()
|
bld := core.NewBufferBuilder().WithFilename("bench.go").WithFiletype("go").WithLines(lines).Build()
|
||||||
buf := &bld
|
buf := &bld
|
||||||
eng := NewTreeSitterEngine(style.DefaultStyles())
|
eng := NewTreeSitterEngine(themes.NewDefaultTheme())
|
||||||
|
|
||||||
eng.PrepareBuffer(buf)
|
eng.PrepareBuffer(buf)
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/style"
|
"git.gophernest.net/azpect/TextEditor/internal/theme/themes"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTreeSitterEngineApplyEditMarksDirtyWithoutFullInvalidation(t *testing.T) {
|
func TestTreeSitterEngineApplyEditMarksDirtyWithoutFullInvalidation(t *testing.T) {
|
||||||
@ -15,7 +15,7 @@ func TestTreeSitterEngineApplyEditMarksDirtyWithoutFullInvalidation(t *testing.T
|
|||||||
Build()
|
Build()
|
||||||
buf := &b
|
buf := &b
|
||||||
|
|
||||||
engine := NewTreeSitterEngine(style.DefaultStyles())
|
engine := NewTreeSitterEngine(themes.NewDefaultTheme())
|
||||||
engine.PrepareBuffer(buf)
|
engine.PrepareBuffer(buf)
|
||||||
|
|
||||||
bc := engine.getCache(buf)
|
bc := engine.getCache(buf)
|
||||||
@ -62,7 +62,7 @@ func TestTreeSitterEngineInvalidateLinesAndBuffer(t *testing.T) {
|
|||||||
Build()
|
Build()
|
||||||
buf := &b
|
buf := &b
|
||||||
|
|
||||||
engine := NewTreeSitterEngine(style.DefaultStyles())
|
engine := NewTreeSitterEngine(themes.NewDefaultTheme())
|
||||||
engine.PrepareBuffer(buf)
|
engine.PrepareBuffer(buf)
|
||||||
|
|
||||||
bc := engine.getCache(buf)
|
bc := engine.getCache(buf)
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
"git.gophernest.net/azpect/TextEditor/internal/style"
|
"git.gophernest.net/azpect/TextEditor/internal/theme/themes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type seqOp func(*core.Buffer, *core.Window)
|
type seqOp func(*core.Buffer, *core.Window)
|
||||||
@ -66,7 +66,7 @@ func TestTreeSitterEngineEditSequences(t *testing.T) {
|
|||||||
w := core.NewWindowBuilder().WithBuffer(buf).WithDimensions(120, 40).Build()
|
w := core.NewWindowBuilder().WithBuffer(buf).WithDimensions(120, 40).Build()
|
||||||
win := &w
|
win := &w
|
||||||
|
|
||||||
engine := NewTreeSitterEngine(style.DefaultStyles())
|
engine := NewTreeSitterEngine(themes.NewDefaultTheme())
|
||||||
|
|
||||||
buf.OnChange = func(change core.BufferChange) {
|
buf.OnChange = func(change core.BufferChange) {
|
||||||
if change.Edit != nil {
|
if change.Edit != nil {
|
||||||
|
|||||||
212
internal/syntax/treesitter_utils.go
Normal file
212
internal/syntax/treesitter_utils.go
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
package syntax
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
sitter "github.com/tree-sitter/go-tree-sitter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// addDirtyRange records a potentially changed line span in the buffer cache.
|
||||||
|
//
|
||||||
|
// The parser/highlighter keeps a list of "dirty" line ranges that must be
|
||||||
|
// reparsed or restyled after edits. This helper makes sure the incoming range
|
||||||
|
// is safe and normalized before storing it:
|
||||||
|
// - nil cache is ignored (defensive early-return)
|
||||||
|
// - start/end are swapped if the caller passed them in reverse order
|
||||||
|
// - negative values are clamped to 0
|
||||||
|
//
|
||||||
|
// After appending the new range, it merges overlaps/adjacent ranges so the
|
||||||
|
// dirty list stays compact and avoids duplicate work during incremental updates.
|
||||||
|
func addDirtyRange(bc *bufferCache, start, end int) {
|
||||||
|
if bc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if end < start {
|
||||||
|
start, end = end, start
|
||||||
|
}
|
||||||
|
start = max(0, start)
|
||||||
|
end = max(0, end)
|
||||||
|
bc.dirty = append(bc.dirty, lineRange{start: start, end: end})
|
||||||
|
bc.dirty = mergeRanges(bc.dirty)
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizedDirtyRanges clamps, filters, and merges dirty ranges for a buffer.
|
||||||
|
//
|
||||||
|
// Tree-sitter and styling operations expect valid row bounds. This function
|
||||||
|
// takes arbitrary line ranges and converts them into a clean canonical form
|
||||||
|
// based on the current buffer size:
|
||||||
|
// - returns nil if there are no lines or no input ranges
|
||||||
|
// - clamps each range to [0, lineCount-1]
|
||||||
|
// - drops invalid ranges where start > end after clamping
|
||||||
|
// - merges overlapping or adjacent ranges
|
||||||
|
//
|
||||||
|
// The returned slice is safe to iterate directly for reparse/restyle passes.
|
||||||
|
func normalizedDirtyRanges(ranges []lineRange, lineCount int) []lineRange {
|
||||||
|
if lineCount <= 0 || len(ranges) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
clamped := make([]lineRange, 0, len(ranges))
|
||||||
|
for _, r := range ranges {
|
||||||
|
start := max(0, r.start)
|
||||||
|
end := min(lineCount-1, r.end)
|
||||||
|
if start > end {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
clamped = append(clamped, lineRange{start: start, end: end})
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergeRanges(clamped)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeRanges sorts and coalesces line ranges into a minimal non-overlapping set.
|
||||||
|
//
|
||||||
|
// Two ranges are merged when they overlap or touch (for example [1,3] and [4,6]
|
||||||
|
// become [1,6]). Treating adjacent ranges as one avoids unnecessary splits in
|
||||||
|
// later highlighting logic.
|
||||||
|
//
|
||||||
|
// Note: this function sorts the provided slice in place before building and
|
||||||
|
// returning a merged result.
|
||||||
|
func mergeRanges(ranges []lineRange) []lineRange {
|
||||||
|
if len(ranges) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(ranges, func(i, j int) bool {
|
||||||
|
if ranges[i].start == ranges[j].start {
|
||||||
|
return ranges[i].end < ranges[j].end
|
||||||
|
}
|
||||||
|
return ranges[i].start < ranges[j].start
|
||||||
|
})
|
||||||
|
|
||||||
|
merged := make([]lineRange, 0, len(ranges))
|
||||||
|
cur := ranges[0]
|
||||||
|
for i := 1; i < len(ranges); i++ {
|
||||||
|
n := ranges[i]
|
||||||
|
if n.start <= cur.end+1 {
|
||||||
|
if n.end > cur.end {
|
||||||
|
cur.end = n.end
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
merged = append(merged, cur)
|
||||||
|
cur = n
|
||||||
|
}
|
||||||
|
merged = append(merged, cur)
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
// rowInRanges reports whether a row index is covered by any range.
|
||||||
|
//
|
||||||
|
// This is a simple membership check used by update paths that need to decide
|
||||||
|
// whether a specific line should be recomputed.
|
||||||
|
func rowInRanges(row int, ranges []lineRange) bool {
|
||||||
|
for _, r := range ranges {
|
||||||
|
if row >= r.start && row <= r.end {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultLineStyles creates a style-per-rune slice initialized with base.
|
||||||
|
//
|
||||||
|
// The highlighter applies styles at rune granularity (not byte granularity) so
|
||||||
|
// multibyte UTF-8 characters still map to exactly one style entry per visible
|
||||||
|
// character. This function produces the baseline style row before syntax
|
||||||
|
// captures overwrite specific spans.
|
||||||
|
func defaultLineStyles(line string, base lipgloss.Style) []lipgloss.Style {
|
||||||
|
runes := []rune(line)
|
||||||
|
row := make([]lipgloss.Style, len(runes))
|
||||||
|
for i := range row {
|
||||||
|
row[i] = base
|
||||||
|
}
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectCaptures consumes a Tree-sitter capture iterator into local ranges.
|
||||||
|
//
|
||||||
|
// For each capture returned by the query iterator, it resolves the capture name
|
||||||
|
// and records start/end row+column coordinates as a captureRange. These ranges
|
||||||
|
// are then used by the renderer to map syntax names to concrete styles.
|
||||||
|
//
|
||||||
|
// Special handling:
|
||||||
|
// - nil query yields nil output
|
||||||
|
// - capture indexes outside query.CaptureNames() are ignored defensively
|
||||||
|
// - captures named "spell" are skipped, because spell-check is handled by a
|
||||||
|
// separate pass and should not be treated as a syntax-highlight capture here
|
||||||
|
func collectCaptures(iter sitter.QueryCaptures, query *sitter.Query) []captureRange {
|
||||||
|
if query == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
names := query.CaptureNames()
|
||||||
|
out := []captureRange{}
|
||||||
|
for match, captureIdx := iter.Next(); match != nil; match, captureIdx = iter.Next() {
|
||||||
|
capture := match.Captures[captureIdx]
|
||||||
|
if int(capture.Index) >= len(names) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := names[capture.Index]
|
||||||
|
if name == "spell" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
node := capture.Node
|
||||||
|
start := node.StartPosition()
|
||||||
|
end := node.EndPosition()
|
||||||
|
out = append(out, captureRange{
|
||||||
|
startRow: start.Row,
|
||||||
|
startCol: start.Column,
|
||||||
|
endRow: end.Row,
|
||||||
|
endCol: end.Column,
|
||||||
|
name: name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildBufferSource flattens the editor buffer into a single newline-delimited
|
||||||
|
// byte slice suitable for Tree-sitter parsing.
|
||||||
|
//
|
||||||
|
// The buffer stores text as lines, while Tree-sitter expects one contiguous
|
||||||
|
// source blob. This helper joins all lines with '\n' separators, preserving row
|
||||||
|
// structure expected by parser positions.
|
||||||
|
func buildBufferSource(buf *core.Buffer) []byte {
|
||||||
|
lineCount := buf.LineCount()
|
||||||
|
if lineCount == 0 {
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := make([]string, lineCount)
|
||||||
|
for i := range lineCount {
|
||||||
|
lines[i] = buf.Line(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(strings.Join(lines, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// byteColToRuneIndex converts a byte-based column offset to a rune index.
|
||||||
|
//
|
||||||
|
// Tree-sitter positions use byte columns, while the renderer/highlighter often
|
||||||
|
// indexes text by runes so multibyte UTF-8 characters are handled correctly.
|
||||||
|
// This conversion keeps style slicing aligned with displayed characters.
|
||||||
|
//
|
||||||
|
// Boundary behavior:
|
||||||
|
// - byteCol <= 0 -> 0
|
||||||
|
// - byteCol >= len(line) -> rune length of the entire line
|
||||||
|
func byteColToRuneIndex(line []byte, byteCol int) int {
|
||||||
|
if byteCol <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if byteCol >= len(line) {
|
||||||
|
return len([]rune(string(line)))
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := line[:byteCol]
|
||||||
|
return len([]rune(string(prefix)))
|
||||||
|
}
|
||||||
@ -1,7 +1,105 @@
|
|||||||
package theme
|
package theme
|
||||||
|
|
||||||
// RegisterAll is retained as a no-op for compatibility while Chroma-based
|
import (
|
||||||
// theme loading is removed.
|
"strings"
|
||||||
func RegisterAll() error {
|
|
||||||
return nil
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EditorTheme struct {
|
||||||
|
Cursors CursorTheme
|
||||||
|
Gutter GutterTheme
|
||||||
|
VisualHightlight lipgloss.Style
|
||||||
|
StatusBar StatusBarTheme
|
||||||
|
CommandLine CommandLineTheme
|
||||||
|
Line lipgloss.Style
|
||||||
|
Background lipgloss.Style
|
||||||
|
Syntax SyntaxTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
type CursorTheme struct {
|
||||||
|
Normal lipgloss.Style
|
||||||
|
Insert lipgloss.Style
|
||||||
|
Command lipgloss.Style
|
||||||
|
Replace lipgloss.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
type GutterTheme struct {
|
||||||
|
Default lipgloss.Style
|
||||||
|
CurrentLine lipgloss.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusBarTheme struct {
|
||||||
|
Default lipgloss.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandLineTheme struct {
|
||||||
|
Error lipgloss.Style
|
||||||
|
OutputBorder lipgloss.Style
|
||||||
|
ContinueMessage lipgloss.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyntaxTheme struct {
|
||||||
|
Exact map[string]lipgloss.Style
|
||||||
|
Group map[string]lipgloss.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t EditorTheme) Cursor(mode core.Mode, textStyle lipgloss.Style) lipgloss.Style {
|
||||||
|
bg := textStyle.GetBackground()
|
||||||
|
fg := textStyle.GetForeground()
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case core.NormalMode, core.VisualLineMode, core.VisualBlockMode, core.VisualMode:
|
||||||
|
return lipgloss.NewStyle().
|
||||||
|
Background(fg).
|
||||||
|
Foreground(bg)
|
||||||
|
case core.ReplaceMode, core.WaitingMode:
|
||||||
|
return textStyle.
|
||||||
|
Underline(true)
|
||||||
|
default:
|
||||||
|
return t.Background.
|
||||||
|
Foreground(fg).
|
||||||
|
Underline(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t EditorTheme) DefaultCursor(mode core.Mode) lipgloss.Style {
|
||||||
|
switch mode {
|
||||||
|
case core.InsertMode:
|
||||||
|
return t.Cursors.Insert
|
||||||
|
case core.CommandMode:
|
||||||
|
return t.Cursors.Command
|
||||||
|
case core.ReplaceMode:
|
||||||
|
return t.Cursors.Replace
|
||||||
|
default:
|
||||||
|
return t.Cursors.Normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method is preferred to raw access of EditorTheme.VisualHightlight since
|
||||||
|
// is has the proper foreground color applied
|
||||||
|
func (t EditorTheme) VisualHighlightWithTextColor(textStyle lipgloss.Style) lipgloss.Style {
|
||||||
|
return t.VisualHightlight.
|
||||||
|
Foreground(textStyle.GetForeground())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use base (Line) as fallback. Every style will use the background from the base (Line).
|
||||||
|
//
|
||||||
|
// NOTE: Maybe we keep background on the mapping? Not sure for now
|
||||||
|
func (t EditorTheme) CaptureStyle(capture string) lipgloss.Style {
|
||||||
|
base := t.Line
|
||||||
|
|
||||||
|
exact := strings.ToLower(strings.TrimSpace(capture))
|
||||||
|
group := strings.Split(exact, ".")[0]
|
||||||
|
|
||||||
|
if sty, ok := t.Syntax.Exact[exact]; ok {
|
||||||
|
return sty.Background(base.GetBackground())
|
||||||
|
}
|
||||||
|
|
||||||
|
if sty, ok := t.Syntax.Group[group]; ok {
|
||||||
|
return sty.Background(base.GetBackground())
|
||||||
|
}
|
||||||
|
|
||||||
|
return base
|
||||||
}
|
}
|
||||||
|
|||||||
196
internal/theme/themes/default.go
Normal file
196
internal/theme/themes/default.go
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
package themes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/theme"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
const background = lipgloss.Color("#111418")
|
||||||
|
const foreground = lipgloss.Color("#d4d8e1")
|
||||||
|
|
||||||
|
func NewDefaultTheme() theme.EditorTheme {
|
||||||
|
hightlight := lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color("#2f334d"))
|
||||||
|
|
||||||
|
line := lipgloss.NewStyle().
|
||||||
|
Foreground(foreground).
|
||||||
|
Background(background)
|
||||||
|
|
||||||
|
background := lipgloss.NewStyle().
|
||||||
|
Background(background)
|
||||||
|
|
||||||
|
return theme.EditorTheme{
|
||||||
|
Cursors: newDefaultCursorTheme(),
|
||||||
|
Gutter: newDefaultGutterTheme(),
|
||||||
|
VisualHightlight: hightlight,
|
||||||
|
StatusBar: newDefaultStatusBarTheme(),
|
||||||
|
CommandLine: newDefaultCommandLineTheme(),
|
||||||
|
Line: line,
|
||||||
|
Background: background,
|
||||||
|
Syntax: newDefaultSyntaxTheme(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is only used for the default cursors, in any other case
|
||||||
|
// the EditorTheme.Cursor() method is preferred.
|
||||||
|
func newDefaultCursorTheme() theme.CursorTheme {
|
||||||
|
base := lipgloss.NewStyle().
|
||||||
|
Foreground(foreground).
|
||||||
|
Background(background)
|
||||||
|
|
||||||
|
inv := lipgloss.NewStyle().
|
||||||
|
Foreground(background).
|
||||||
|
Background(foreground)
|
||||||
|
|
||||||
|
return theme.CursorTheme{
|
||||||
|
Normal: inv,
|
||||||
|
Insert: base.Underline(true),
|
||||||
|
Command: inv,
|
||||||
|
Replace: base.Underline(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDefaultGutterTheme() theme.GutterTheme {
|
||||||
|
base := lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color("#0d1014")).
|
||||||
|
Foreground(lipgloss.Color("#6b7280"))
|
||||||
|
|
||||||
|
return theme.GutterTheme{
|
||||||
|
Default: base,
|
||||||
|
CurrentLine: base.Foreground(lipgloss.Color("#c0c8d8")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDefaultStatusBarTheme() theme.StatusBarTheme {
|
||||||
|
bar := lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color("#0d1014")).
|
||||||
|
Foreground(lipgloss.Color("#8f99aa"))
|
||||||
|
|
||||||
|
return theme.StatusBarTheme{
|
||||||
|
Default: bar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDefaultCommandLineTheme() theme.CommandLineTheme {
|
||||||
|
base := lipgloss.NewStyle().
|
||||||
|
Foreground(foreground).
|
||||||
|
Background(background)
|
||||||
|
|
||||||
|
return theme.CommandLineTheme{
|
||||||
|
Error: base.Foreground(lipgloss.Color("#bf616a")),
|
||||||
|
OutputBorder: base.Background(lipgloss.Color("#0d1014")),
|
||||||
|
ContinueMessage: base.Foreground(lipgloss.Color("#81a1c1")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDefaultSyntaxTheme() theme.SyntaxTheme {
|
||||||
|
exact := map[string]lipgloss.Style{
|
||||||
|
"attribute.builtin": color("#ebcb8b"),
|
||||||
|
"character.special": color("#d08770"),
|
||||||
|
"comment.documentation": color("#8f99aa"),
|
||||||
|
"constant.builtin": color("#88c0d0"),
|
||||||
|
"constant.macro": color("#d08770"),
|
||||||
|
"function.builtin": color("#88c0d0"),
|
||||||
|
"function.call": color("#81a1c1"),
|
||||||
|
"function.macro": color("#d08770"),
|
||||||
|
"function.method": color("#81a1c1"),
|
||||||
|
"function.method.call": color("#81a1c1"),
|
||||||
|
"keyword.conditional": color("#b48ead"),
|
||||||
|
"keyword.conditional.ternary": color("#b48ead"),
|
||||||
|
"keyword.coroutine": color("#b48ead"),
|
||||||
|
"keyword.debug": color("#b48ead"),
|
||||||
|
"keyword.directive": color("#b48ead"),
|
||||||
|
"keyword.directive.define": color("#b48ead"),
|
||||||
|
"keyword.exception": color("#b48ead"),
|
||||||
|
"keyword.function": color("#b48ead"),
|
||||||
|
"keyword.import": color("#b48ead"),
|
||||||
|
"keyword.modifier": color("#b48ead"),
|
||||||
|
"keyword.operator": color("#d08770"),
|
||||||
|
"keyword.repeat": color("#b48ead"),
|
||||||
|
"keyword.return": color("#b48ead"),
|
||||||
|
"keyword.type": color("#ebcb8b"),
|
||||||
|
"markup.heading": color("#ebcb8b"),
|
||||||
|
"markup.heading.1": color("#ebcb8b"),
|
||||||
|
"markup.heading.2": color("#e5c68a"),
|
||||||
|
"markup.heading.3": color("#ddbe88"),
|
||||||
|
"markup.heading.4": color("#d5b686"),
|
||||||
|
"markup.heading.5": color("#cdaf84"),
|
||||||
|
"markup.heading.6": color("#c5a883"),
|
||||||
|
"markup.italic": color("#c0c8d8"),
|
||||||
|
"markup.link.label": color("#81a1c1"),
|
||||||
|
"markup.raw": color("#a3be8c"),
|
||||||
|
"markup.strikethrough": color("#7f8795"),
|
||||||
|
"markup.strong": color("#ebcb8b"),
|
||||||
|
"markup.underline": color("#88c0d0"),
|
||||||
|
"module.builtin": color("#88c0d0"),
|
||||||
|
"number.float": color("#88c0d0"),
|
||||||
|
"punctuation.bracket": color("#9aa4b2"),
|
||||||
|
"punctuation.delimiter": color("#9aa4b2"),
|
||||||
|
"punctuation.special": color("#d08770"),
|
||||||
|
"string.documentation": color("#a3be8c"),
|
||||||
|
"string.escape": color("#d08770"),
|
||||||
|
"string.regexp": color("#88c0d0"),
|
||||||
|
"string.special.path": color("#a3be8c"),
|
||||||
|
"string.special.symbol": color("#88c0d0"),
|
||||||
|
"string.special.url": color("#88c0d0"),
|
||||||
|
"tag.attribute": color("#ebcb8b"),
|
||||||
|
"tag.attribute.url": color("#88c0d0"),
|
||||||
|
"tag.builtin": color("#81a1c1"),
|
||||||
|
"tag.delimiter": color("#9aa4b2"),
|
||||||
|
"type.builtin": color("#ebcb8b"),
|
||||||
|
"type.definition": color("#ebcb8b"),
|
||||||
|
"variable.builtin": color("#8fbcbb"),
|
||||||
|
"variable.member": color("#c0c8d8"),
|
||||||
|
"variable.parameter": color("#c0c8d8"),
|
||||||
|
}
|
||||||
|
|
||||||
|
group := map[string]lipgloss.Style{
|
||||||
|
"attribute": color("#ebcb8b"),
|
||||||
|
"boolean": color("#88c0d0"),
|
||||||
|
"character": color("#a3be8c"),
|
||||||
|
"charset": color("#ebcb8b"),
|
||||||
|
"comment": color("#7f8795"),
|
||||||
|
"conceal": color("#7f8795"),
|
||||||
|
"constant": color("#88c0d0"),
|
||||||
|
"constructor": color("#ebcb8b"),
|
||||||
|
"error": color("#bf616a"),
|
||||||
|
"function": color("#81a1c1"),
|
||||||
|
"import": color("#b48ead"),
|
||||||
|
"interface": color("#ebcb8b"),
|
||||||
|
"keyframes": color("#d08770"),
|
||||||
|
"keyword": color("#b48ead"),
|
||||||
|
"label": color("#d08770"),
|
||||||
|
"media": color("#d08770"),
|
||||||
|
"module": color("#81a1c1"),
|
||||||
|
"namespace": color("#81a1c1"),
|
||||||
|
"none": color("#d4d8e1"),
|
||||||
|
"nospell": color("#d4d8e1"),
|
||||||
|
"number": color("#88c0d0"),
|
||||||
|
"operator": color("#9aa4b2"),
|
||||||
|
"property": color("#c0c8d8"),
|
||||||
|
"spell": color("#d4d8e1"),
|
||||||
|
"string": color("#a3be8c"),
|
||||||
|
"supports": color("#d08770"),
|
||||||
|
"tag": color("#81a1c1"),
|
||||||
|
"type": color("#ebcb8b"),
|
||||||
|
"variable": color("#d4d8e1"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return theme.SyntaxTheme{
|
||||||
|
Exact: exact,
|
||||||
|
Group: group,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple helper to create a lipgloss style with the provided foreground
|
||||||
|
func color(c string) lipgloss.Style {
|
||||||
|
col := foreground
|
||||||
|
if strings.TrimSpace(c) != "" {
|
||||||
|
col = lipgloss.Color(c)
|
||||||
|
}
|
||||||
|
return lipgloss.NewStyle().
|
||||||
|
Background(background).
|
||||||
|
Foreground(col)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user