From 6ca1fa504336730b7870f013830dfc5009c3464b Mon Sep 17 00:00:00 2001 From: topi314 Date: Mon, 18 Nov 2024 03:17:17 +0100 Subject: [PATCH] fix highlighting --- config/language_servers.toml | 2 +- config/queries/go/highlights.scm | 6 +- config/themes/dark.toml | 22 +++ gopad/actions.go | 12 ++ gopad/config/config.go | 2 + gopad/config/languages.go | 6 + gopad/config/styles.go | 36 ++++ gopad/editor/buffer/buffer.go | 10 +- gopad/editor/buffer/byte_line.go | 10 +- gopad/editor/buffer/line_buffer.go | 17 +- gopad/editor/document_view.go | 116 ++++++++----- gopad/editor/editor.go | 75 ++++++-- gopad/editor/file/document.go | 71 +++++--- gopad/editor/file/highlight.go | 189 +++++++++++--------- gopad/editor/file/languages.go | 3 +- gopad/editor/file/syntax.go | 35 ++-- gopad/editor/file_msgs.go | 18 ++ gopad/gopad.go | 13 +- internal/rope/rope.go | 270 +++++++++++++++++++++++++++++ internal/rope/rope_reader.go | 44 +++++ internal/xbytes/bytes.go | 21 +++ 21 files changed, 788 insertions(+), 190 deletions(-) create mode 100644 internal/rope/rope.go create mode 100644 internal/rope/rope_reader.go diff --git a/config/language_servers.toml b/config/language_servers.toml index 2adc734..4fa1050 100644 --- a/config/language_servers.toml +++ b/config/language_servers.toml @@ -1,7 +1,7 @@ # Language Server Protocol configuration # For a list of available language servers, see: https://langserver.org/#implementations-server -use_servers = { only = ['gopls', 'golangci-lint', 'superhtml'], except = [] } +use_servers = { only = ['gopls'], except = [] } [language_servers.gopls] command = 'gopls' diff --git a/config/queries/go/highlights.scm b/config/queries/go/highlights.scm index fee598b..4172dcd 100644 --- a/config/queries/go/highlights.scm +++ b/config/queries/go/highlights.scm @@ -171,7 +171,7 @@ "func" @keyword.function -"return" @keyword.return +"return" @keyword.control.return [ "import" @@ -183,9 +183,9 @@ "case" "switch" "if" - ] @keyword.conditional + ] @keyword.control.conditional -"for" @keyword.repeat +"for" @keyword.control.repeat [ "var" diff --git a/config/themes/dark.toml b/config/themes/dark.toml index 791cb4d..6cb9733 100644 --- a/config/themes/dark.toml +++ b/config/themes/dark.toml @@ -261,15 +261,32 @@ unnecessary_char = { curly_underline = true, underline_color = '$red' } "constant.numeric.float" = { foreground = '$yellow' } "constant.builtin" = { foreground = '$bright_red' } "constant.builtin.boolean" = { foreground = '$yellow' } +"constant.character" = { foreground = '$yellow' } "constant.character.escape" = { foreground = '$cyan', bold = true } "constant.other.placeholder" = { foreground = '$cyan', bold = true } "function" = { foreground = '$bright_cyan' } "function.builtin" = { foreground = '$bright_cyan' } +"function.method" = { foreground = '$bright_cyan' } +"function.method.private" = { foreground = '$bright_cyan' } "function.macro" = { foreground = '$bright_cyan' } + "constructor" = { foreground = '$bright_cyan' } + "keyword" = { foreground = '$bright_magenta', italic = true } +"keyword.control" = { foreground = '$bright_magenta', italic = true } +"keyword.control.conditional" = { foreground = '$bright_magenta', italic = true } +"keyword.control.repeat" = { foreground = '$bright_magenta', italic = true } +"keyword.control.import" = { foreground = '$bright_magenta', italic = true } +"keyword.control.return" = { foreground = '$bright_magenta', italic = true } +"keyword.control.exception" = { foreground = '$bright_magenta', italic = true } +"keyword.operator" = { foreground = '$bright_magenta', italic = true } "keyword.directive" = { foreground = '$bright_magenta', italic = true } +"keyword.function" = { foreground = '$bright_magenta', italic = true } +"keyword.storage" = { foreground = '$bright_magenta', italic = true } +"keyword.storage.type" = { foreground = '$bright_magenta', italic = true } +"keyword.storage.modifier" = { foreground = '$bright_magenta', italic = true } + "section" = { foreground = '$bright_magenta' } "punctuation.delimiter" = { foreground = '$white' } @@ -277,12 +294,17 @@ unnecessary_char = { curly_underline = true, underline_color = '$red' } "punctuation.bracket" = { foreground = '$white' } "operator" = { foreground = '$white' } + "special" = { foreground = '$bright_cyan' } + "string" = { foreground = '$bright_green', bold = true } "string.special" = { foreground = '$yellow' } "string.special.path" = { foreground = '$bright_green', bold = true } "string.special.url" = { foreground = '$blue' } + "type" = { foreground = '$bright_yellow', bold = true } +"type.builtin" = { foreground = '$bright_yellow', bold = true } +"type.parameter" = { foreground = '$bright_yellow', bold = true } "markup.heading" = { foreground = '$cyan' } "markup.heading.marker" = { foreground = '$bright_magenta' } diff --git a/gopad/actions.go b/gopad/actions.go index ca4d454..90f3e15 100644 --- a/gopad/actions.go +++ b/gopad/actions.go @@ -126,6 +126,18 @@ var Actions = []Action{ return OpenLSPOverlay }, }, + { + Name: "Stop LSP", + Run: func() tea.Cmd { + return OpenLSPOverlay + }, + }, + { + Name: "Format Document", + Run: func() tea.Cmd { + return editor.FormatAction + }, + }, } type Action struct { diff --git a/gopad/config/config.go b/gopad/config/config.go index f7e638a..97e692b 100644 --- a/gopad/config/config.go +++ b/gopad/config/config.go @@ -31,6 +31,7 @@ var ( Keys Keymap Keymaps []KeymapConfig Theme ThemeStyles + CodeTheme *CodeStyles Themes []ThemeConfig ) @@ -109,6 +110,7 @@ func Load(name string, defaultConfigs embed.FS) error { } } Theme = theme.Theme() + CodeTheme = NewCodeStyles(Theme.CodeStyles) return nil } diff --git a/gopad/config/languages.go b/gopad/config/languages.go index 6ed27ea..44bc271 100644 --- a/gopad/config/languages.go +++ b/gopad/config/languages.go @@ -52,6 +52,7 @@ type LanguageConfig struct { AutoPairs []LanguageAutoPairs `toml:"auto_pairs"` Indent Indent `toml:"indent"` Grammar *GrammarConfig `toml:"grammar"` + Formatter *FormatterConfig `toml:"formatter"` } type BlockCommentToken struct { @@ -108,3 +109,8 @@ const ( RefTypeCommit RefType = "commit" RefTypeTag RefType = "tag" ) + +type FormatterConfig struct { + Command string `toml:"command"` + Args []string `toml:"args"` +} diff --git a/gopad/config/styles.go b/gopad/config/styles.go index 8508a37..2f03d99 100644 --- a/gopad/config/styles.go +++ b/gopad/config/styles.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "image/color" "github.com/charmbracelet/lipgloss/v2" @@ -17,6 +18,41 @@ import ( "go.gopad.dev/gopad/internal/bubbles/textinput" ) +func NewCodeStyles(styles map[string]lipgloss.Style) *CodeStyles { + var scopes []string + + for key := range styles { + scopes = append(scopes, key) + } + + return &CodeStyles{ + styles: styles, + scopes: scopes, + } +} + +type CodeStyles struct { + styles map[string]lipgloss.Style + scopes []string +} + +func (t *CodeStyles) Highlight(i int, languageName string) lipgloss.Style { + scope := t.scopes[i] + style, ok := t.styles[fmt.Sprintf("%s.%s", scope, languageName)] + if ok { + return style + } + return t.styles[scope] +} + +func (t *CodeStyles) Scope(i int) string { + return t.scopes[i] +} + +func (t *CodeStyles) Scopes() []string { + return t.scopes +} + type ThemeStyles struct { Name string Foreground color.Color diff --git a/gopad/editor/buffer/buffer.go b/gopad/editor/buffer/buffer.go index 0aad69a..b4c03e2 100644 --- a/gopad/editor/buffer/buffer.go +++ b/gopad/editor/buffer/buffer.go @@ -66,8 +66,10 @@ type Buffer interface { // Rune returns the rune at the given index. Rune(i int) rune - // ByteIndex returns the byte index in the buffer for the rune index. + // ByteIndex converts the rune index to a byte index. ByteIndex(i int) int + // RuneIndex converts the byte index to a rune index. + RuneIndex(i int) int // ByteIndexByPoint returns the byte index in the buffer for the given point. ByteIndexByPoint(p Point) int // Position returns the point for the given byte index. @@ -103,8 +105,10 @@ type Line interface { Len() int // BytesLen returns the byte length of the line. BytesLen() int - // Index returns the byte index in the line for the given rune index. - Index(i int) int + // ByteIndex converts the rune index to a byte index. + ByteIndex(i int) int + // RuneIndex converts the byte index to a rune index. + RuneIndex(i int) int // Rune returns the rune at the given index. Rune(i int) rune diff --git a/gopad/editor/buffer/byte_line.go b/gopad/editor/buffer/byte_line.go index d221d1b..ff9e8fb 100644 --- a/gopad/editor/buffer/byte_line.go +++ b/gopad/editor/buffer/byte_line.go @@ -38,12 +38,16 @@ func (l byteLine) BytesLen() int { return len(l.data) } -func (l byteLine) Index(index int) int { +func (l byteLine) Rune(index int) rune { + return xbytes.Rune(l.data, index) +} + +func (l byteLine) ByteIndex(index int) int { return xbytes.RuneIndex(l.data, index) } -func (l byteLine) Rune(index int) rune { - return xbytes.Rune(l.data, index) +func (l byteLine) RuneIndex(index int) int { + return xbytes.RuneIndex(l.data, index) } func (l byteLine) Runes() []rune { diff --git a/gopad/editor/buffer/line_buffer.go b/gopad/editor/buffer/line_buffer.go index 19de2cf..7be78f6 100644 --- a/gopad/editor/buffer/line_buffer.go +++ b/gopad/editor/buffer/line_buffer.go @@ -199,13 +199,24 @@ func (b *lineBuffer) Rune(i int) rune { return 0 } -// ByteIndex returns the byte index in the buffer for the rune index. func (b *lineBuffer) ByteIndex(i int) int { var n int for _, line := range b.lines { - byteLen := line.BytesLen() + runeLen := line.Len() + 1 + if n+runeLen >= i { + return n + line.ByteIndex(i-n) + } + n += runeLen + } + return n +} + +func (b *lineBuffer) RuneIndex(i int) int { + var n int + for _, line := range b.lines { + byteLen := len(line.Bytes()) if n+byteLen >= i { - return n + line.Index(i-n) + return n + line.RuneIndex(i-n) } n += byteLen + 1 } diff --git a/gopad/editor/document_view.go b/gopad/editor/document_view.go index 1843a72..687ed65 100644 --- a/gopad/editor/document_view.go +++ b/gopad/editor/document_view.go @@ -6,7 +6,6 @@ import ( "log" "strconv" "strings" - "time" "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" @@ -512,23 +511,6 @@ func (v DocumentView) Update(msg tea.Msg) (DocumentView, tea.Cmd) { return v, tea.Batch(cmds...) case key.Matches(msg, config.Keys.Editor.RefreshSyntaxHighlight): // TODO: refresh syntax highlight - - case key.Matches(msg, config.Keys.Editor.DebugTreeSitterNodes): - // TODO: decide where to put this - // if v.file.Tree == nil { - // cmds = append(cmds, notifications.Add("no tree available for this file")) - // return v, tea.Batch(cmds...) - // } - // buff, err := buffer.New(v.file.Buffer.FileName()+".tree", bytes.NewReader([]byte(v.file.Tree.Print())), "utf-8", buffer.LineEndingLF, false) - // if err != nil { - // cmds = append(cmds, notifications.Add(fmt.Sprintf("error while opening tree.scm: %s", err.Error()))) - // return v, tea.Batch(cmds...) - // } - // - // debugFile := file.NewDocumentWithBuffer(buff, file.ModeReadOnly) - // - // e.files = append(e.files, debugFile) - // e.activeFile = len(e.files) - 1 case key.Matches(msg, config.Keys.Editor.Diagnostic.Show): v.ShowCurrentDiagnostic() case key.Matches(msg, config.Keys.Cancel) && v.ShowsCurrentDiagnostic(): @@ -747,23 +729,7 @@ func (v DocumentView) Update(msg tea.Msg) (DocumentView, tea.Cmd) { return v, tea.Batch(cmds...) } -func getStyle(f func() (file.CharStyle, bool)) file.CharStyle { - style, ok := f() - if !ok { - log.Println("no more styles") - return file.CharStyle{ - Style: lipgloss.NewStyle(), - End: 0, - } - } - log.Println("found style", style.Style) - return style -} - func (v DocumentView) View(width int, height int, border bool, debug bool) string { - now := time.Now() - defer log.Println("DocumentView.View took", time.Since(now)) - styles := config.Theme.UI borderStyle := func(strs ...string) string { return strings.Join(strs, " ") } if border { @@ -773,18 +739,25 @@ func (v DocumentView) View(width int, height int, border bool, debug bool) strin prefixWidth := lipgloss.Width(strconv.Itoa(v.file.Buffer.LinesLen())) width = max(width-prefixWidth-styles.FileView.BorderStyle.GetHorizontalFrameSize()-3, 0) + // debug takes up 4 lines + if debug { + height = max(height-4, 0) + } + v.refreshCursorViewOffset(width-2, height) c := v.Cursor() offset := v.cursor.offset selection := v.Selection() - nextStyle, _ := iter.Pull(v.file.HighlightIter(nil)) - //defer stop() - charStyle := getStyle(nextStyle) + nextStyle, stop := iter.Pull(v.file.HighlightIter(nil)) + defer stop() + charStyle, _ := nextStyle() var ( editorCode string lineCode string + + cursorCharStyle file.CharStyle ) r := buffer.NewReader(v.file.Buffer, offset) for { @@ -793,34 +766,52 @@ func (v DocumentView) View(width int, height int, border bool, debug bool) strin break } - if char.Point.Row == height { + // stop rendering if we are out of the visible area + if char.Point.Row < offset.Row || char.Point.Row >= offset.Row+height { break } if char.Rune == '\n' { + if char.Point.Row == c.Row && char.Point.Col == c.Col { + lineCode += v.cursor.cursor.View(" ", charStyle.Style) + cursorCharStyle = charStyle + } else { + lineCode += charStyle.Style.Render(" ") + } + editorCode += borderStyle(lineCode) + "\n" lineCode = "" continue } - //if char.Point.Col-offset.Col < width || char.Point.Row < offset.Row { - // continue - //} + // only render visible lines + if char.Point.Col < offset.Col || char.Point.Col >= offset.Col+width { + continue + } if char.Index >= charStyle.End { for { - charStyle = getStyle(nextStyle) + charStyle, ok = nextStyle() + if !ok { + break + } if char.Index < charStyle.End { break } } } + // replace tabs with spaces for now TODO: handle tabs properly + if char.Rune == '\t' { + char.Rune = ' ' + } + inSelection := selection != nil && selection.Contains(char.Point) var renderChar string if char.Point.Row == c.Row && char.Point.Col == c.Col { renderChar = v.cursor.cursor.View(string(char.Rune), charStyle.Style) + cursorCharStyle = charStyle } else if inSelection { renderChar = styles.FileView.SelectionStyle.Inherit(charStyle.Style).Render(string(char.Rune)) } else { @@ -836,5 +827,46 @@ func (v DocumentView) View(width int, height int, border bool, debug bool) strin editorCode = strings.TrimSuffix(editorCode, "\n") + if v.showCurrentDiagnostic { + diagnostic := v.file.HighestLineColDiagnostic(c.Row, c.Col) + if diagnostic.Severity > 0 { + editorCode = overlay.PlacePosition(lipgloss.Left, lipgloss.Top, diagnostic.View(width, height), editorCode, + overlay.WithMarginX(styles.FileView.LinePrefixStyle.GetHorizontalFrameSize()+prefixWidth+1+c.Col), + overlay.WithMarginY(c.Row+1), + ) + } else { + v.HideCurrentDiagnostic() + } + } else if v.file.Autocomplete.Visible() { + editorCode = overlay.PlacePosition(lipgloss.Left, lipgloss.Top, v.file.Autocomplete.View(width, height), editorCode, + overlay.WithMarginX(styles.FileView.LinePrefixStyle.GetHorizontalFrameSize()+prefixWidth+1+c.Col), + overlay.WithMarginY(c.Row+1), + ) + } + + if debug { + editorCode += "\n" + borderStyle(fmt.Sprintf(" Cursor Char Style: %s (%s) [%d]", cursorCharStyle.StyleName, cursorCharStyle.LanguageName, cursorCharStyle.End)) + + diagnostics := v.file.DiagnosticsForLineCol(c.Row, c.Col) + var currentDiagnostics []string + for _, diag := range diagnostics { + currentDiagnostics = append(currentDiagnostics, fmt.Sprintf("%s (%s: %s [%d, %d] - [%d, %d])", diag.Message, diag.Type, diag.Source, diag.Range.Start.Row, diag.Range.Start.Col, diag.Range.End.Row, diag.Range.End.Col)) + } + editorCode += "\n" + borderStyle(fmt.Sprintf(" Current Diagnostics: %s", strings.Join(currentDiagnostics, ", "))) + + hints := v.file.InlayHintsForLine(c.Row) + var currentHints []string + for _, hint := range hints { + currentHints = append(currentHints, fmt.Sprintf("%s (%s [%d, %d])", hint.Label, hint.Type, hint.Position.Row, hint.Position.Col)) + } + editorCode += "\n" + borderStyle(fmt.Sprintf(" Current Inlay Hints: %s", strings.Join(currentHints, ", "))) + + var currentDefinitions []string + for _, definitions := range v.file.Definitions { + currentDefinitions = append(currentDefinitions, fmt.Sprintf("%s ([%d, %d] - [%d, %d])", definitions.Name, definitions.Range.Start.Row, definitions.Range.Start.Col, definitions.Range.End.Row, definitions.Range.End.Col)) + } + editorCode += "\n" + borderStyle(fmt.Sprintf(" Current Definitions: %s", strings.Join(currentDefinitions, ", "))) + } + return editorCode } diff --git a/gopad/editor/editor.go b/gopad/editor/editor.go index 07aa54a..50ffd3f 100644 --- a/gopad/editor/editor.go +++ b/gopad/editor/editor.go @@ -102,9 +102,9 @@ func (e Editor) Init() (Editor, tea.Cmd) { } if f := e.FileView(); f != nil { - cmds = append(cmds, e.Focus(ModelTypeFile)) + cmds = append(cmds, Focus(ModelTypeFile)) } else { - cmds = append(cmds, e.Focus(ModelTypeFileTree)) + cmds = append(cmds, Focus(ModelTypeFileTree)) } return e, tea.Batch(cmds...) @@ -214,8 +214,21 @@ func (e *Editor) OpenFile(name string) (tea.Cmd, error) { return tea.Batch(cmds...), nil } +func (e *Editor) FormatFile(name string) (tea.Cmd, error) { + f := e.ViewByName(name) + if f == nil { + return nil, nil + } + cmd, err := f.file.Format() + if err != nil { + return cmd, err + } + + return tea.Batch(cmd, ls.FileCreated(f.file.Name, f.file.Buffer.Bytes())), nil +} + func (e *Editor) SaveFile(name string) (tea.Cmd, error) { - f := e.FileByName(name) + f := e.ViewByName(name) if f == nil { return nil, nil } @@ -231,7 +244,7 @@ func (e *Editor) RenameFile(oldName string, newName string) (tea.Cmd, error) { newName = filepath.Join(e.workspace, newName) newName, _ = filepath.Abs(newName) } - f := e.FileByName(oldName) + f := e.ViewByName(oldName) if f == nil { return nil, nil } @@ -293,11 +306,11 @@ func (e *Editor) FileView() *DocumentView { return e.fileViews[e.activeFile] } -func (e *Editor) SetFile(index int) { +func (e *Editor) SetView(index int) { e.activeFile = index } -func (e *Editor) SetFileByName(name string) { +func (e *Editor) SetViewByName(name string) { for i, f := range e.fileViews { if f.file.Name == name { e.activeFile = i @@ -306,7 +319,7 @@ func (e *Editor) SetFileByName(name string) { } } -func (e *Editor) FileByName(name string) *DocumentView { +func (e *Editor) ViewByName(name string) *DocumentView { for _, f := range e.fileViews { if f.file.Name == name { return f @@ -337,6 +350,11 @@ func (e Editor) Update(msg tea.Msg) (Editor, tea.Cmd) { cmds = append(cmds, v.SetLanguage(msg.Language)) } return e, tea.Batch(cmds...) + case FormatActionMsg: + if v := e.FileView(); v != nil { + cmds = append(cmds, FormatFile(v.Name())) + } + return e, tea.Batch(cmds...) case SaveActionMsg: if v := e.FileView(); v != nil { cmds = append(cmds, SaveFile(v.Name())) @@ -383,7 +401,7 @@ func (e Editor) Update(msg tea.Msg) (Editor, tea.Cmd) { case CutMsg: if v := e.FileView(); v != nil { - //v.file.DeleteRange(buffer.Range(msg)) + // v.file.DeleteRange(buffer.Range(msg)) v.ResetMark() } return e, tea.Batch(cmds...) @@ -433,11 +451,22 @@ func (e Editor) Update(msg tea.Msg) (Editor, tea.Cmd) { notifications.Add(fmt.Sprintf("file %s opened", msg.Name)), Focus(ModelTypeFile), ) - e.SetFileByName(msg.Name) + e.SetViewByName(msg.Name) if msg.Position != nil { e.FileView().SetCursor(*msg.Position) } return e, tea.Batch(cmds...) + case FormatFileMsg: + cmd, err := e.FormatFile(msg.Name) + if err != nil { + cmds = append(cmds, notifications.Add(fmt.Sprintf("error while formatting file %s: %s", msg.Name, err.Error()))) + return e, tea.Batch(cmds...) + } + if cmd != nil { + cmds = append(cmds, cmd) + } + cmds = append(cmds, notifications.Add(fmt.Sprintf("file %s formatted", msg.Name))) + return e, tea.Batch(cmds...) case SaveFileMsg: cmd, err := e.SaveFile(msg.Name) if err != nil { @@ -462,7 +491,7 @@ func (e Editor) Update(msg tea.Msg) (Editor, tea.Cmd) { notifications.Add(fmt.Sprintf("file %s created", msg.Name)), Focus(ModelTypeFile), ) - e.SetFileByName(msg.Name) + e.SetViewByName(msg.Name) return e, tea.Batch(cmds...) case RenameFileMsg: cmd, err := e.RenameFile(msg.OldName, msg.NewName) @@ -524,7 +553,7 @@ func (e Editor) Update(msg tea.Msg) (Editor, tea.Cmd) { cmds = append(cmds, Focus(ModelTypeFile)) i, _ := strconv.Atoi(strings.TrimPrefix(z.ID(), ZoneFilePrefix)) - e.SetFile(i) + e.SetView(i) return e, tea.Batch(cmds...) case mouse.MatchesZone(msg, z, tea.MouseRight): // TODO: open context menu? @@ -591,6 +620,30 @@ func (e Editor) Update(msg tea.Msg) (Editor, tea.Cmd) { case key.Matches(msg, config.Keys.Editor.ToggleTreeSitterDebug): e.ToggleTreeSitterDebug() return e, tea.Batch(cmds...) + case key.Matches(msg, config.Keys.Editor.DebugTreeSitterNodes): + v := e.FileView() + if v != nil { + if v.file.Syntax == nil { + cmds = append(cmds, notifications.Add("no syntax tree available for this file")) + return e, tea.Batch(cmds...) + } + tree := v.file.Syntax.Layers.Tree() + + buff, err := buffer.New(bytes.NewReader([]byte(tree.RootNode().ToSexp())), buffer.LineEndingLF) + if err != nil { + cmds = append(cmds, notifications.Addf("error while creating tree s-expression buffer: %s", err.Error())) + return e, tea.Batch(cmds...) + } + + dv, err := newDocumentView(v.file.FileName()+".tree", buff, file.ModeReadOnly) + if err != nil { + cmds = append(cmds, notifications.Addf("error while creating tree s-expression view: %s", err.Error())) + return e, tea.Batch(cmds...) + } + + e.fileViews = append(e.fileViews, dv) + e.activeFile = len(e.fileViews) - 1 + } case key.Matches(msg, config.Keys.Editor.File.Next): if e.activeFile < len(e.fileViews)-1 { if f := e.FileView(); f != nil { diff --git a/gopad/editor/file/document.go b/gopad/editor/file/document.go index 9fae1bb..cce0550 100644 --- a/gopad/editor/file/document.go +++ b/gopad/editor/file/document.go @@ -1,18 +1,22 @@ package file import ( + "bytes" "context" + "errors" "fmt" + "io" "iter" "log" "os" + "os/exec" "path/filepath" "slices" "time" "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/lipgloss" + "go.gopad.dev/gopad/gopad/config" "go.gopad.dev/gopad/gopad/editor/buffer" "go.gopad.dev/gopad/gopad/ls" "go.gopad.dev/gopad/internal/xrunes" @@ -27,26 +31,23 @@ const ( ModeWrite ) -func NewDocumentWithBuffer(name string, b buffer.Buffer, mode Mode) (*Document, error) { +func NewDocumentWithBuffer(name string, buf buffer.Buffer, mode Mode) (*Document, error) { var syntax *Syntax if language := GetLanguageByFilename(name); language != nil { - layers, err := NewSyntaxLayers(b.Bytes(), language.Grammar.Highlight) - if err != nil { - return nil, fmt.Errorf("error creating syntax layers: %w", err) - } - syntax, err = NewSyntax(language, layers) + var err error + syntax, err = NewSyntax(language, buf.Bytes()) if err != nil { return nil, fmt.Errorf("error creating syntax: %w", err) } } d := &Document{ - Buffer: b, + Buffer: buf, Name: name, Mode: mode, Syntax: syntax, oldState: nil, - changes: NewChangeSetFromBuf(b), + changes: NewChangeSetFromBuf(buf), History: NewHistory(), Autocomplete: nil, version: 0, @@ -144,12 +145,7 @@ func (d *Document) SetLanguage(name string) error { return fmt.Errorf("language with name %q not found", name) } - layers, err := NewSyntaxLayers(d.Buffer.Bytes(), language.Grammar.Highlight) - if err != nil { - return fmt.Errorf("error creating syntax layers: %w", err) - } - - syntax, err := NewSyntax(language, layers) + syntax, err := NewSyntax(language, d.Buffer.Bytes()) if err != nil { return fmt.Errorf("error creating syntax: %w", err) } @@ -470,6 +466,42 @@ func (d *Document) NextWordRight(p buffer.Point) buffer.Point { return p } +func (d *Document) Format() (tea.Cmd, error) { + if d.Syntax == nil { + return nil, errors.New("no syntax configured") + } + + formatter := d.Syntax.Language.Config.Formatter + if formatter == nil { + return nil, errors.New("no formatter configured") + } + + cmd := exec.Command(formatter.Command, formatter.Args...) + cmd.Stdin = bytes.NewReader(d.Buffer.Bytes()) + out, err := cmd.StdoutPipe() + if err != nil { + return nil, errors.New("error creating formatter output pipe") + } + + if err = cmd.Start(); err != nil { + return nil, errors.New("error starting formatter") + } + + newData, err := io.ReadAll(out) + if err != nil { + return nil, errors.New("error reading formatter output") + } + + if err = cmd.Wait(); err != nil { + return nil, errors.New("error waiting for formatter") + } + + return d.Replace(buffer.Range{ + Start: buffer.Point{Row: 0, Col: 0}, + End: buffer.Point{Row: d.Buffer.LinesLen(), Col: d.Buffer.LineLen(max(d.Buffer.LinesLen()-1, 0))}, + }, newData), nil +} + // TODO: implement func (d *Document) Save() error { return nil @@ -489,7 +521,6 @@ func (d *Document) HighlightIter(r *ByteRange) iter.Seq[CharStyle] { var hIter iter.Seq2[HighlightEvent, error] if d.Syntax == nil { - log.Println("no syntax available, highlighting entire buffer") hIter = func(yield func(HighlightEvent, error) bool) { yield(HighlightEventSource{ StartByte: 0, @@ -497,14 +528,8 @@ func (d *Document) HighlightIter(r *ByteRange) iter.Seq[CharStyle] { }, nil) } } else { - log.Println("highlighting with syntax") hIter = d.Syntax.Layers.HighlightIter(context.Background(), d.Buffer.Bytes(), r) } - return newStyleIter(hIter, d.Buffer) -} - -type CharStyle struct { - Style lipgloss.Style - End int + return newStyleIter(hIter, d.Buffer, config.CodeTheme) } diff --git a/gopad/editor/file/highlight.go b/gopad/editor/file/highlight.go index 6c964b9..c725736 100644 --- a/gopad/editor/file/highlight.go +++ b/gopad/editor/file/highlight.go @@ -25,6 +25,8 @@ const ( captureLocalScopeInherits = "local.scope-inherits" ) +type Highlight uint + type HighlightEvent interface { highlightEvent() } @@ -37,7 +39,7 @@ type HighlightEventSource struct { func (HighlightEventSource) highlightEvent() {} type HighlightEventStart struct { - CaptureName string + Highlight Highlight LanguageName string } @@ -47,8 +49,9 @@ type HighlightEventEnd struct{} func (HighlightEventEnd) highlightEvent() {} -func NewHighlightConfig(language *tree_sitter.Language, languageName string, highlightsQuery []byte, injectionQuery []byte, localsQuery []byte) (*HighlightConfig, error) { - querySource := localsQuery +func NewHighlightConfig(language *tree_sitter.Language, languageName string, highlightsQuery []byte, injectionQuery []byte, localsQuery []byte) (*HighlightConfiguration, error) { + var querySource []byte + querySource = append(querySource, localsQuery...) highlightsQueryOffset := uint(len(querySource)) querySource = append(querySource, highlightsQuery...) @@ -98,7 +101,6 @@ func NewHighlightConfig(language *tree_sitter.Language, languageName string, hig localScopeCaptureIndex *uint ) - highlightIndices := make([]string, 0) for i, captureName := range query.CaptureNames() { ui := uint(i) switch captureName { @@ -114,12 +116,11 @@ func NewHighlightConfig(language *tree_sitter.Language, languageName string, hig localRefCaptureIndex = &ui case "local.scope": localScopeCaptureIndex = &ui - default: - highlightIndices = append(highlightIndices, captureName) } } - return &HighlightConfig{ + highlightIndices := make([]*Highlight, len(query.CaptureNames())) + return &HighlightConfiguration{ Language: language, LanguageName: languageName, Query: query, @@ -137,14 +138,14 @@ func NewHighlightConfig(language *tree_sitter.Language, languageName string, hig }, nil } -type HighlightConfig struct { +type HighlightConfiguration struct { Language *tree_sitter.Language LanguageName string Query *tree_sitter.Query InjectionsQuery *tree_sitter.Query CombinedInjectionsPatterns []uint HighlightsPatternIndex uint - HighlightIndices []string + HighlightIndices []*Highlight NonLocalVariablePatterns []bool InjectionContentCaptureIndex *uint InjectionLanguageCaptureIndex *uint @@ -154,10 +155,49 @@ type HighlightConfig struct { LocalRefCaptureIndex *uint } +func (c *HighlightConfiguration) Configure(recognizedNames []string) { + highlightIndices := make([]*Highlight, len(c.Query.CaptureNames())) + for i, captureName := range c.Query.CaptureNames() { + captureParts := strings.Split(captureName, ".") + + var bestIndex *Highlight + var bestMatchLen int + for j, recognizedName := range recognizedNames { + var matchLen int + matches := true + for _, part := range strings.Split(recognizedName, ".") { + matchLen++ + if !slices.Contains(captureParts, part) { + matches = false + break + } + } + if matches && matchLen > bestMatchLen { + index := Highlight(j) + bestIndex = &index + bestMatchLen = matchLen + } + } + highlightIndices[i] = bestIndex + } + c.HighlightIndices = highlightIndices +} + +func (c *HighlightConfiguration) NonconformantCaptureNames(captureNames []string) []string { + var nonconformantNames []string + for _, name := range c.Query.CaptureNames() { + if !(strings.HasPrefix(name, "_") || strings.HasPrefix(name, "local") || slices.Contains(captureNames, name)) { + nonconformantNames = append(nonconformantNames, name) + } + } + + return nonconformantNames +} + type LocalDef struct { Name string Range ByteRange - Highlight *string + Highlight *Highlight } type LocalScope struct { @@ -166,7 +206,7 @@ type LocalScope struct { LocalDefs []LocalDef } -type InjectionCallback func(name string) *HighlightConfig +type InjectionCallback func(name string) *HighlightConfiguration type iterRange struct { Start uint @@ -212,6 +252,10 @@ func (h *highlightIter) sortLayers() { i++ continue } + } else { + layer := h.Layers[i+1] + h.Layers = append(h.Layers[:i], h.Layers[i+1:]...) + h.Syntax.parser.pushCursor(layer.Cursor) } break } @@ -219,10 +263,11 @@ func (h *highlightIter) sortLayers() { h.Layers = append(h.Layers[:i], append([]*highlightIterLayer{h.Layers[0]}, h.Layers[i:]...)...) } break + } else { + layer := h.Layers[0] + h.Layers = h.Layers[1:] + h.Syntax.parser.pushCursor(layer.Cursor) } - layer := h.Layers[0] - h.Layers = h.Layers[1:] - h.Syntax.parser.pushCursor(layer.Cursor) } } @@ -244,18 +289,17 @@ main: // If none of the layers have any more highlight boundaries, terminate. if len(h.Layers) == 0 { - log.Println("no more highlight boundaries") - if h.ByteOffset < uint(len(h.Source)) { - event := HighlightEventSource{ + sourceLen := uint(len(h.Source)) + if h.ByteOffset < sourceLen { + result := HighlightEventSource{ StartByte: h.ByteOffset, - EndByte: uint(len(h.Source)), + EndByte: sourceLen, } - h.ByteOffset = uint(len(h.Source)) - return event, nil + h.ByteOffset = sourceLen + return result, nil } return nil, nil } - log.Println("LAYER", h.Layers[0]) // Get the next capture from whichever layer has the earliest highlight boundary. var r tree_sitter.Range @@ -275,14 +319,14 @@ main: return h.emitEvent(endByte, HighlightEventEnd{}) } } - } else if len(layer.HighlightEndStack) > 0 { + } else { // If there are no more captures, then emit any remaining highlight end events. // And if there are none of those, then just advance to the end of the document. - - endByte := layer.HighlightEndStack[len(layer.HighlightEndStack)-1] - layer.HighlightEndStack = layer.HighlightEndStack[:len(layer.HighlightEndStack)-1] - return h.emitEvent(endByte, HighlightEventEnd{}) - } else { + if len(layer.HighlightEndStack) > 0 { + endByte := layer.HighlightEndStack[len(layer.HighlightEndStack)-1] + layer.HighlightEndStack = layer.HighlightEndStack[:len(layer.HighlightEndStack)-1] + return h.emitEvent(endByte, HighlightEventEnd{}) + } return h.emitEvent(uint(len(h.Source)), nil) } @@ -297,8 +341,8 @@ main: // If this capture is for tracking local variables, then process the // local variable info. - var referenceHighlight *string - var definitionHighlight *string + var referenceHighlight *Highlight + var definitionHighlight *Highlight for match.Match.PatternIndex < layer.Config.HighlightsPatternIndex { // If the node represents a local scope, push a new local scope onto // the scope stack. @@ -346,7 +390,7 @@ main: if len(h.Source) > int(r.StartByte) && len(h.Source) > int(r.EndByte) { name := string(h.Source[r.StartByte:r.EndByte]) for _, scope := range slices.Backward(layer.ScopeStack) { - var highlight *string + var highlight *Highlight for _, def := range slices.Backward(scope.LocalDefs) { if def.Name == name && r.StartByte >= def.Range.EndByte { highlight = def.Highlight @@ -421,13 +465,13 @@ main: // If this node represents a local definition, then store the current // highlight value on the local scope entry representing this node. if definitionHighlight != nil { - definitionHighlight = ¤tHighlight + definitionHighlight = currentHighlight } // Emit a scope start event and push the node's end position to the stack. highlight := referenceHighlight if highlight == nil { - highlight = ¤tHighlight + highlight = currentHighlight } if highlight != nil { h.LastHighlightRange = &iterRange{ @@ -437,7 +481,7 @@ main: } layer.HighlightEndStack = append(layer.HighlightEndStack, r.EndByte) return h.emitEvent(r.StartByte, HighlightEventStart{ - CaptureName: *highlight, + Highlight: *highlight, LanguageName: layer.Config.LanguageName, }) } @@ -544,7 +588,7 @@ func intersectRanges(parentRanges []tree_sitter.Range, nodes []tree_sitter.Node, return results } -func (c HighlightConfig) injectionForMatch(query *tree_sitter.Query, match *tree_sitter.QueryMatch, source []byte) (string, *tree_sitter.Node, bool) { +func (c HighlightConfiguration) injectionForMatch(query *tree_sitter.Query, match *tree_sitter.QueryMatch, source []byte) (string, *tree_sitter.Node, bool) { if c.InjectionContentCaptureIndex == nil || c.InjectionLanguageCaptureIndex == nil { return "", nil, false } @@ -590,7 +634,7 @@ type queryCapture struct { type highlightIterLayer struct { Tree *tree_sitter.Tree Cursor *tree_sitter.QueryCursor - Config HighlightConfig + Config HighlightConfiguration HighlightEndStack []uint ScopeStack []LocalScope Captures []queryCapture @@ -651,55 +695,34 @@ func (h *highlightIterLayer) sortKey() *sortKeyResult { } } -func newStyleIter(highlightIter iter.Seq2[HighlightEvent, error], buf buffer.Buffer) iter.Seq[CharStyle] { +type CharStyle struct { + Style lipgloss.Style + StyleName string + LanguageName string + End int +} + +func newStyleIter(highlightIter iter.Seq2[HighlightEvent, error], buf buffer.Buffer, styles *config.CodeStyles) iter.Seq[CharStyle] { iterator := styleIterator{ - textStyle: lipgloss.NewStyle().Foreground(config.Theme.Foreground).Background(config.Theme.Background), activeHighlights: nil, highlightIter: highlightIter, buf: buf, - theme: config.Theme.CodeStyles, + styles: styles, } return iterator.iter() } -type highlight struct { - captureName string +type highlightStyle struct { + highlight Highlight languageName string } type styleIterator struct { - textStyle lipgloss.Style - activeHighlights []highlight + activeHighlights []highlightStyle highlightIter iter.Seq2[HighlightEvent, error] buf buffer.Buffer - theme map[string]lipgloss.Style -} - -func (i *styleIterator) highlight(captureName string, languageName string) lipgloss.Style { - log.Println("looking for style for", captureName, languageName) - var style lipgloss.Style - - for { - codeStyle, ok := i.theme[fmt.Sprintf("%s.%s", captureName, languageName)] - if ok { - style = codeStyle - break - } - codeStyle, ok = i.theme[captureName] - if ok { - style = codeStyle - break - } - lastDot := strings.LastIndex(captureName, ".") - if lastDot == -1 { - break - } - captureName = captureName[:lastDot] - } - - log.Println("no style in theme for", captureName, languageName) - return style + styles *config.CodeStyles } func (i *styleIterator) iter() iter.Seq[CharStyle] { @@ -709,26 +732,32 @@ func (i *styleIterator) iter() iter.Seq[CharStyle] { log.Printf("error getting highlight event: %v", err) continue } - log.Println("event", event) switch event := event.(type) { case HighlightEventStart: - i.activeHighlights = append(i.activeHighlights, highlight{ - captureName: event.CaptureName, + i.activeHighlights = append(i.activeHighlights, highlightStyle{ + highlight: event.Highlight, languageName: event.LanguageName, }) case HighlightEventEnd: i.activeHighlights = i.activeHighlights[:len(i.activeHighlights)-1] case HighlightEventSource: - var style lipgloss.Style - for _, h := range i.activeHighlights { - style = i.highlight(h.captureName, h.languageName) + ch := CharStyle{ + // End: i.buf.RuneIndex(int(event.EndByte)), TODO: RuneIndex seems to be broken, investigate + End: int(event.EndByte), + } + + if len(i.activeHighlights) > 0 { + highlight := i.activeHighlights[len(i.activeHighlights)-1] + + ch.Style = i.styles.Highlight(int(highlight.highlight), highlight.languageName) + ch.StyleName = i.styles.Scope(int(highlight.highlight)) + ch.LanguageName = highlight.languageName + } + + if ok := yield(ch); !ok { + return } - end := i.buf.ByteIndex(int(event.EndByte)) - yield(CharStyle{ - Style: style, - End: end, - }) } } } diff --git a/gopad/editor/file/languages.go b/gopad/editor/file/languages.go index 6971e85..8512a2f 100644 --- a/gopad/editor/file/languages.go +++ b/gopad/editor/file/languages.go @@ -46,7 +46,7 @@ func (l *Language) Description() string { } type GrammarConfig struct { - Highlight HighlightConfig + Highlight HighlightConfiguration Outline *OutlineQueryConfig } @@ -71,6 +71,7 @@ func LoadLanguages(defaultConfigs embed.FS) error { return fmt.Errorf("error loading tree-sitter grammar for %q: %w", name, err) } if g != nil { + g.Highlight.Configure(config.CodeTheme.Scopes()) lang.Grammar = g } } diff --git a/gopad/editor/file/syntax.go b/gopad/editor/file/syntax.go index 88441aa..4e3ae9b 100644 --- a/gopad/editor/file/syntax.go +++ b/gopad/editor/file/syntax.go @@ -3,6 +3,7 @@ package file import ( "context" "encoding/binary" + "fmt" "iter" "log" "slices" @@ -20,7 +21,7 @@ const ( TreeSitterMatchLimit = 256 ) -var injectionCallback = func(languageName string) *HighlightConfig { +var injectionCallback = func(languageName string) *HighlightConfiguration { language := GetLanguage(languageName) if language == nil || language.Grammar == nil { return nil @@ -50,16 +51,22 @@ type ByteRange struct { EndByte uint } -func NewSyntax(language *Language, layers *SyntaxLayers) (*Syntax, error) { - layers, err := NewSyntaxLayers(nil, language.Grammar.Highlight) +func NewSyntax(language *Language, source []byte) (*Syntax, error) { + layers, err := NewSyntaxLayers(source, language.Grammar.Highlight) if err != nil { - return nil, err + return nil, fmt.Errorf("error creating syntax layers: %w", err) } - return &Syntax{ + syntax := &Syntax{ Language: language, Layers: layers, - }, nil + } + + if err = syntax.Parse(context.Background(), 0, source, nil); err != nil { + return nil, fmt.Errorf("error parsing syntax: %w", err) + } + + return syntax, nil } type Syntax struct { @@ -125,7 +132,7 @@ func (p *parser) popCursor() *tree_sitter.QueryCursor { return cursor } -func NewSyntaxLayers(source []byte, config HighlightConfig) (*SyntaxLayers, error) { +func NewSyntaxLayers(source []byte, config HighlightConfiguration) (*SyntaxLayers, error) { rootLayer := &LanguageLayer{ Config: config, Tree: nil, @@ -171,7 +178,7 @@ type SyntaxLayers struct { } type injectionItem struct { - config HighlightConfig + config HighlightConfiguration ranges []tree_sitter.Range } @@ -243,8 +250,8 @@ func (s *SyntaxLayers) Update(ctx context.Context, currentRev uint64, newRev uin if cursor == nil { cursor = tree_sitter.NewQueryCursor() } - //cursor.SetByteRange(0, ^uint(0)) - //cursor.SetMatchLimit(TreeSitterMatchLimit) + // cursor.SetByteRange(0, ^uint(0)) + // cursor.SetMatchLimit(TreeSitterMatchLimit) touched := map[slotmap.LayerID]struct{}{} @@ -404,8 +411,9 @@ func (s *SyntaxLayers) HighlightIter(ctx context.Context, source []byte, r *Byte EndByte: ^uint(0), } } - //cursor.SetByteRange(r.StartByte, r.EndByte) - //cursor.SetMatchLimit(TreeSitterMatchLimit) + + // cursor.SetByteRange(r.StartByte, r.EndByte) + // cursor.SetMatchLimit(TreeSitterMatchLimit) captures := make([]queryCapture, 0) queryCaptures := cursor.Captures(layer.Config.Query, layer.Tree.RootNode(), source) @@ -451,6 +459,7 @@ func (s *SyntaxLayers) HighlightIter(ctx context.Context, source []byte, r *Byte Layers: layers, NextEvent: nil, LastHighlightRange: nil, + Syntax: s, } hIter.sortLayers() @@ -491,7 +500,7 @@ func pointSub(a tree_sitter.Point, b tree_sitter.Point) tree_sitter.Point { } type LanguageLayer struct { - Config HighlightConfig + Config HighlightConfiguration Tree *tree_sitter.Tree Ranges []tree_sitter.Range Depth int diff --git a/gopad/editor/file_msgs.go b/gopad/editor/file_msgs.go index 140be84..3ea41b4 100644 --- a/gopad/editor/file_msgs.go +++ b/gopad/editor/file_msgs.go @@ -11,6 +11,12 @@ import ( "go.gopad.dev/gopad/internal/bubbles/notifications" ) +func FormatAction() tea.Msg { + return FormatActionMsg{} +} + +type FormatActionMsg struct{} + func SaveAction() tea.Msg { return SaveActionMsg{} } @@ -149,6 +155,18 @@ type OpenFileMsg struct { Position *buffer.Point } +func FormatFile(name string) tea.Cmd { + return func() tea.Msg { + return FormatFileMsg{ + Name: name, + } + } +} + +type FormatFileMsg struct { + Name string +} + func SaveFile(name string) tea.Cmd { return func() tea.Msg { return SaveFileMsg{ diff --git a/gopad/gopad.go b/gopad/gopad.go index 2819360..85addfd 100644 --- a/gopad/gopad.go +++ b/gopad/gopad.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" @@ -86,10 +87,10 @@ func (g Gopad) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if _, ok := msg.(cursor.BlinkMsg); !ok { log.Printf("Msg: %T: %v\n", msg, msg) } - //now := time.Now() - //defer func() { + // now := time.Now() + // defer func() { // log.Printf("Update time: %s\nMessage: %T", time.Since(now), msg) - //}() + // }() var cmds []tea.Cmd @@ -201,10 +202,8 @@ func (g Gopad) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (g Gopad) View() string { - //now := time.Now() - //defer func() { - // log.Printf("Render time: %s\n", time.Since(now)) - //}() + now := time.Now() + defer log.Printf("Render time: %s\n", time.Since(now)) height := g.height diff --git a/internal/rope/rope.go b/internal/rope/rope.go new file mode 100644 index 0000000..ad76216 --- /dev/null +++ b/internal/rope/rope.go @@ -0,0 +1,270 @@ +package rope + +import ( + "bytes" + "strings" +) + +const ( + maxDepth = 64 + maxLeafSize = 4096 +) + +func New() Rope { + return Rope{} +} + +func NewString(s string) Rope { + return Rope{content: s, len: len(s)} +} + +func NewBytes(b []byte) Rope { + return NewString(string(b)) +} + +type Rope struct { + content string + len int + depth int + left *Rope + right *Rope +} + +func (r Rope) concat(r2 Rope) Rope { + switch { + case r.len == 0: + return r2 + case r2.len == 0: + return r + case r.len+r2.len <= maxLeafSize: + return NewString(r.String() + r2.String()) + default: + depth := r.depth + if r2.depth > depth { + depth = r2.depth + } + return Rope{ + len: r.len + r2.len, + depth: depth + 1, + left: &r, + right: &r2, + } + } +} + +func (r Rope) Append(r2 Rope) Rope { + return r.concat(r2).rebalanceIfNeeded() +} + +func (r Rope) AppendString(s string) Rope { + return r.Append(NewString(s)) +} + +func (r Rope) AppendBytes(b []byte) Rope { + return r.Append(NewBytes(b)) +} + +func (r Rope) Delete(offset int, length int) Rope { + if length == 0 || offset == r.len { + return r + } + + left, right := r.Split(offset) + _, newRight := right.Split(length) + return left.Append(newRight) +} + +func (r Rope) Equal(r2 Rope) bool { + if r == r2 { + return true + } + + if r.len != r2.len { + return false + } + + for i := 0; i < r.len; i += maxLeafSize { + if !bytes.Equal(r.Slice(i, i+maxLeafSize), r2.Slice(i, i+maxLeafSize)) { + return false + } + } + + return true +} + +func (r Rope) Insert(at int, other Rope) Rope { + switch at { + case 0: + return other.Append(r) + case r.len: + return r.Append(other) + default: + left, right := r.Split(at) + return left.concat(other).Append(right) + } +} + +func (r Rope) InsertString(at int, s string) Rope { + return r.Insert(at, NewString(s)) +} + +func (r Rope) InsertBytes(at int, b []byte) Rope { + return r.Insert(at, NewBytes(b)) +} + +func (r Rope) Len() int { + return r.len +} + +func (r Rope) Rebalance() Rope { + if r.isBalanced() { + return r + } + + var leaves []Rope + r.walk(func(node Rope) { + leaves = append(leaves, node) + }) + + return merge(leaves, 0, len(leaves)) +} + +func (r Rope) Slice(a, b int) []byte { + p := make([]byte, b-a) + n, _ := r.ReadAt(p, int64(a)) + return p[:n] +} + +func (r Rope) Split(at int) (Rope, Rope) { + switch { + case r.isLeaf(): + return NewString(r.content[0:at]), NewString(r.content[at:]) + + case at == 0: + return Rope{}, r + + case at == r.len: + return r, Rope{} + + case at < r.left.len: + left, right := r.left.Split(at) + return left, right.Append(*r.right) + + case at > r.left.len: + left, right := r.right.Split(at - r.left.len) + return r.left.Append(left), right + + default: + return *r.left, *r.right + } +} + +func (r Rope) String() string { + if r.isLeaf() { + return r.content + } + + var builder strings.Builder + r.walk(func(node Rope) { + builder.WriteString(node.content) + }) + + return builder.String() +} + +func (r Rope) Bytes() []byte { + if r.isLeaf() { + return []byte(r.content) + } + + var b []byte + r.walk(func(node Rope) { + b = append(b, []byte(node.content)...) + }) + + return b +} + +func (r Rope) isBalanced() bool { + switch { + case r.isLeaf(): + return true + case r.depth >= len(fibonacci)-2: + return false + default: + return fibonacci[r.depth+2] <= r.len + } +} + +func (r Rope) isLeaf() bool { + return r.left == nil +} + +func (r Rope) leafForOffset(at int) (Rope, int) { + switch { + case r.isLeaf(): + return r, at + case at < r.left.len: + return r.left.leafForOffset(at) + default: + return r.right.leafForOffset(at - r.left.len) + } +} + +func (r Rope) rebalanceIfNeeded() Rope { + if r.isBalanced() || abs(r.left.depth-r.right.depth) < maxDepth { + return r + } + + return r.Rebalance() +} + +func (r Rope) walk(callback func(Rope)) { + if r.isLeaf() { + callback(r) + } else { + r.left.walk(callback) + r.right.walk(callback) + } +} + +func abs(a int) int { + if a < 0 { + return -a + } + return a +} + +func merge(leaves []Rope, start int, end int) Rope { + length := end - start + switch length { + case 1: + return leaves[start] + case 2: + return leaves[start].concat(leaves[start+1]) + default: + mid := start + length/2 + return merge(leaves, start, mid).concat(merge(leaves, mid, end)) + } +} + +var fibonacci []int + +func init() { + // The heurstic for whether a rope is balanced depends on the Fibonacci sequence; + // we initialize the table of Fibonacci numbers here. + first := 0 + second := 1 + + for c := 0; c < maxDepth+3; c++ { + next := 0 + if c <= 1 { + next = c + } else { + next = first + second + first = second + second = next + } + fibonacci = append(fibonacci, next) + } +} diff --git a/internal/rope/rope_reader.go b/internal/rope/rope_reader.go new file mode 100644 index 0000000..4d0b123 --- /dev/null +++ b/internal/rope/rope_reader.go @@ -0,0 +1,44 @@ +package rope + +import ( + "io" +) + +func NewReader(rope Rope) *Reader { + return rope.Reader() +} + +type Reader struct { + rope Rope + position int64 +} + +func (r *Reader) Read(p []byte) (n int, err error) { + n, err = r.rope.ReadAt(p, r.position) + if err == nil { + r.position += int64(n) + } + return +} + +func (r Rope) Reader() *Reader { + return r.OffsetReader(0) +} + +func (r Rope) OffsetReader(offset int) *Reader { + return &Reader{rope: r, position: int64(offset)} +} + +func (r Rope) ReadAt(p []byte, off int64) (n int, err error) { + o := int(off) + for n < len(p) && o+n < r.Len() { + leaf, at := r.leafForOffset(o + n) + n += copy(p[n:], leaf.content[at:]) + } + + if n < len(p) { + err = io.EOF + } + + return +} diff --git a/internal/xbytes/bytes.go b/internal/xbytes/bytes.go index 21b11fb..8634260 100644 --- a/internal/xbytes/bytes.go +++ b/internal/xbytes/bytes.go @@ -5,6 +5,27 @@ import ( "unicode/utf8" ) +// ByteIndex converts the rune index to a byte index. +func ByteIndex(s []byte, i int) int { + if i == 0 { + return 0 + } + + runeIndex := 0 + for len(s) > 0 { + _, l := utf8.DecodeRune(s) + s = s[l:] + + if runeIndex == i { + return l + } + + runeIndex++ + } + return -1 +} + +// RuneIndex converts the byte index to a rune index. func RuneIndex(s []byte, i int) int { if i == 0 { return 0