feat: Implemented syntax styles

Treesitter integration implemented! But tests are failing, need to
resolve that.
This commit is contained in:
Hayden Hargreaves 2026-04-07 22:34:42 -07:00
parent 77416bc0a4
commit 1c2585b8d9
5 changed files with 177 additions and 36 deletions

View File

@ -15,6 +15,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() editorStyles := style.DefaultStyles()
editorTheme := themes.NewDefaultTheme()
return &ModelBuilder{ return &ModelBuilder{
model: Model{ model: Model{
@ -34,8 +35,8 @@ func NewModelBuilder() *ModelBuilder {
settings: core.NewDefaultSettings(), settings: core.NewDefaultSettings(),
registers: core.DefaultRegisters(), registers: core.DefaultRegisters(),
styles: editorStyles, styles: editorStyles,
syntax: syntax.NewTreeSitterEngine(editorStyles), syntax: syntax.NewTreeSitterEngine(editorTheme),
theme: themes.NewDefaultTheme(), theme: editorTheme,
}, },
} }
} }

View File

@ -31,8 +31,8 @@ func (m Model) View() string {
// Draw window // Draw window
view := viewWindow(win, t, 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
@ -225,38 +225,37 @@ func rightBar(w *core.Window, mode core.Mode, t theme.EditorTheme) (bar string)
// 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
} }

View File

@ -5,7 +5,7 @@ import (
"sort" "sort"
"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"
) )
@ -20,8 +20,8 @@ import (
// //
// Cached styles are represented as one style per rune for each line. // 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
} }
@ -72,11 +72,11 @@ type captureRange struct {
// //
// Language support is resolved through the language registry, so the engine can // Language support is resolved through the language registry, so the engine can
// work with any language/query pair registered there. // 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{},
} }
} }
@ -138,7 +138,7 @@ 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
@ -312,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)
} }
} }
} }
@ -397,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

View File

@ -1,6 +1,8 @@
package theme package theme
import ( import (
"strings"
"git.gophernest.net/azpect/TextEditor/internal/core" "git.gophernest.net/azpect/TextEditor/internal/core"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
@ -13,6 +15,7 @@ type EditorTheme struct {
CommandLine CommandLineTheme CommandLine CommandLineTheme
Line lipgloss.Style Line lipgloss.Style
Background lipgloss.Style Background lipgloss.Style
Syntax SyntaxTheme
} }
type CursorTheme struct { type CursorTheme struct {
@ -37,6 +40,11 @@ type CommandLineTheme struct {
ContinueMessage 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 { func (t EditorTheme) Cursor(mode core.Mode, textStyle lipgloss.Style) lipgloss.Style {
bg := textStyle.GetBackground() bg := textStyle.GetBackground()
fg := textStyle.GetForeground() fg := textStyle.GetForeground()
@ -75,3 +83,23 @@ func (t EditorTheme) VisualHighlightWithTextColor(textStyle lipgloss.Style) lipg
return t.VisualHightlight. return t.VisualHightlight.
Foreground(textStyle.GetForeground()) 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
}

View File

@ -1,12 +1,14 @@
package themes package themes
import ( import (
"strings"
"git.gophernest.net/azpect/TextEditor/internal/theme" "git.gophernest.net/azpect/TextEditor/internal/theme"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
const background = lipgloss.Color("#1f2335") const background = lipgloss.Color("#111418")
const foreground = lipgloss.Color("#dcd7ba") const foreground = lipgloss.Color("#d4d8e1")
func NewDefaultTheme() theme.EditorTheme { func NewDefaultTheme() theme.EditorTheme {
hightlight := lipgloss.NewStyle(). hightlight := lipgloss.NewStyle().
@ -27,6 +29,7 @@ func NewDefaultTheme() theme.EditorTheme {
CommandLine: newDefaultCommandLineTheme(), CommandLine: newDefaultCommandLineTheme(),
Line: line, Line: line,
Background: background, Background: background,
Syntax: newDefaultSyntaxTheme(),
} }
} }
@ -51,19 +54,19 @@ func newDefaultCursorTheme() theme.CursorTheme {
func newDefaultGutterTheme() theme.GutterTheme { func newDefaultGutterTheme() theme.GutterTheme {
base := lipgloss.NewStyle(). base := lipgloss.NewStyle().
Background(lipgloss.Color("#181b2a")). Background(lipgloss.Color("#0d1014")).
Foreground(lipgloss.Color("#7e8399")) Foreground(lipgloss.Color("#6b7280"))
return theme.GutterTheme{ return theme.GutterTheme{
Default: base, Default: base,
CurrentLine: base.Foreground(lipgloss.Color("#f6c384")), CurrentLine: base.Foreground(lipgloss.Color("#c0c8d8")),
} }
} }
func newDefaultStatusBarTheme() theme.StatusBarTheme { func newDefaultStatusBarTheme() theme.StatusBarTheme {
bar := lipgloss.NewStyle(). bar := lipgloss.NewStyle().
Background(lipgloss.Color("#181b2a")). Background(lipgloss.Color("#0d1014")).
Foreground(lipgloss.Color("#8ea4a2")) Foreground(lipgloss.Color("#8f99aa"))
return theme.StatusBarTheme{ return theme.StatusBarTheme{
Default: bar, Default: bar,
@ -76,8 +79,118 @@ func newDefaultCommandLineTheme() theme.CommandLineTheme {
Background(background) Background(background)
return theme.CommandLineTheme{ return theme.CommandLineTheme{
Error: base.Foreground(lipgloss.Color("#e82424")), Error: base.Foreground(lipgloss.Color("#bf616a")),
OutputBorder: base.Background(lipgloss.Color("#11131d")), OutputBorder: base.Background(lipgloss.Color("#0d1014")),
ContinueMessage: base.Foreground(lipgloss.Color("#7aa2f7")), 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)
}