mirror of
https://github.com/EnumeratedDev/typer.git
synced 2026-09-16 05:36:10 +00:00
685 lines
16 KiB
Go
685 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"typer/runestring"
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
)
|
|
|
|
type Buffer struct {
|
|
Name string
|
|
Contents []runestring.RuneString
|
|
|
|
CursorPos Position
|
|
Offset Position
|
|
|
|
Selection *Selection
|
|
|
|
canSave bool
|
|
canEdit bool
|
|
filetype string
|
|
filename string
|
|
}
|
|
|
|
type Selection struct {
|
|
selectionStart, selectionEnd Position
|
|
}
|
|
|
|
var Buffers = make([]*Buffer, 0)
|
|
|
|
func GetBufferByName(name string) *Buffer {
|
|
for _, buffer := range Buffers {
|
|
if buffer.Name == name {
|
|
return buffer
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GetBufferByFilename(filename string) *Buffer {
|
|
for _, buffer := range Buffers {
|
|
if buffer.filename == filename {
|
|
return buffer
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func drawBuffer(window *Window) {
|
|
buffer := window.CurrentBuffer
|
|
|
|
x, y, _, _ := window.GetTextAreaDimensions()
|
|
|
|
bufferX, bufferY, _, _ := window.GetTextAreaDimensions()
|
|
|
|
parsedSyntaxes, err := HighlightString(string(buffer.GetContentsAsString()), buffer.filetype)
|
|
if err != nil {
|
|
window.PrintMessage(fmt.Sprintf("Could not parse regular expression in '%s' syntax: %s", buffer.filetype, err), TYPER_MESSAGE_ERROR)
|
|
}
|
|
|
|
i := -1
|
|
for lineIndex, line := range buffer.Contents {
|
|
for runeIndex, r := range append(line, ' ') {
|
|
i++
|
|
drawPosition := Position{runeIndex, lineIndex}
|
|
|
|
if x-buffer.Offset.X >= bufferX && y-buffer.Offset.Y >= bufferY {
|
|
// Default style
|
|
style := tcell.StyleDefault.Background(CurrentStyle.BufferAreaBg).Foreground(CurrentStyle.BufferAreaFg)
|
|
|
|
// Check for syntax highlighting
|
|
for _, parsedSyntax := range parsedSyntaxes {
|
|
if i >= parsedSyntax.StartIndex && i < parsedSyntax.EndIndex {
|
|
switch parsedSyntax.Type {
|
|
case "comment":
|
|
style = style.Foreground(CurrentStyle.SyntaxComment)
|
|
case "keyword":
|
|
style = style.Foreground(CurrentStyle.SyntaxKeyword)
|
|
case "identifier":
|
|
style = style.Foreground(CurrentStyle.SyntaxIdentifier)
|
|
case "constant":
|
|
style = style.Foreground(CurrentStyle.SyntaxConstant)
|
|
case "variable":
|
|
style = style.Foreground(CurrentStyle.SyntaxVariable)
|
|
case "string":
|
|
style = style.Foreground(CurrentStyle.SyntaxString)
|
|
|
|
// Special types for typer logs
|
|
case "info":
|
|
style = style.Foreground(CurrentStyle.SyntaxInfo)
|
|
case "warning":
|
|
style = style.Foreground(CurrentStyle.SyntaxWarning)
|
|
case "error":
|
|
style = style.Foreground(CurrentStyle.SyntaxError)
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
// Change background if under cursor
|
|
if buffer.CursorPos.Equals(runeIndex, lineIndex) {
|
|
style = style.Background(CurrentStyle.BufferAreaSel)
|
|
}
|
|
|
|
// Change background if selected
|
|
if buffer.Selection != nil {
|
|
edge1, edge2 := buffer.GetSelectionEdges()
|
|
|
|
if ComparePositions(drawPosition, edge1) >= 0 && ComparePositions(drawPosition, edge2) <= 0 {
|
|
style = style.Background(CurrentStyle.BufferAreaSel)
|
|
|
|
// Show selection on entire tab space
|
|
if r == '\t' {
|
|
for j := 0; j < int(Config.TabIndentation); j++ {
|
|
window.screen.SetContent(x+j-buffer.Offset.X, y-buffer.Offset.Y, r, nil, style)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
window.screen.SetContent(x-buffer.Offset.X, y-buffer.Offset.Y, r, nil, style)
|
|
}
|
|
|
|
// Change position for next character
|
|
if r == '\t' {
|
|
x += int(Config.TabIndentation)
|
|
} else {
|
|
x++
|
|
}
|
|
}
|
|
// Draw new line
|
|
x = bufferX
|
|
y++
|
|
}
|
|
|
|
}
|
|
|
|
func (buffer *Buffer) Load() error {
|
|
// Do not load if canSave is false or filename is not set
|
|
if !buffer.canSave || buffer.filename == "" {
|
|
return nil
|
|
}
|
|
|
|
// Replace tilde with home directory
|
|
if strings.HasPrefix(buffer.filename, "~/") {
|
|
homedir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
buffer.filename = filepath.Join(homedir, buffer.filename[2:])
|
|
}
|
|
|
|
contentBytes, err := os.ReadFile(buffer.filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
content := runestring.RuneString(string(contentBytes))
|
|
|
|
if len(content) != 0 {
|
|
buffer.Contents = runestring.Split(content, '\n')
|
|
|
|
// Add empty line at end of buffer for last newline
|
|
if content[len(content)-1] == '\n' {
|
|
buffer.Contents = append(buffer.Contents, make(runestring.RuneString, 0))
|
|
}
|
|
|
|
buffer.CursorPos.Y = len(buffer.Contents) - 1
|
|
buffer.CursorPos.X = len(buffer.Contents[buffer.CursorPos.Y])
|
|
}
|
|
|
|
// Set buffer filetype
|
|
for _, syntax := range AvailableSyntaxes {
|
|
if syntax.Filenames == "" {
|
|
continue
|
|
}
|
|
|
|
if ok, _ := regexp.MatchString(syntax.Filenames, buffer.filename); ok {
|
|
buffer.filetype = syntax.Filetype
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (buffer *Buffer) Save() error {
|
|
// Do not save if canSave is false or filename is not set
|
|
if !buffer.canSave || buffer.filename == "" {
|
|
return nil
|
|
}
|
|
|
|
// Replace tilde with home directory
|
|
if strings.HasPrefix(buffer.filename, "~/") {
|
|
homedir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
buffer.filename = filepath.Join(homedir, buffer.filename[2:])
|
|
}
|
|
|
|
// Add newline at the end of buffer Contents
|
|
line := buffer.Contents[len(buffer.Contents)-1]
|
|
if len(line) != 0 {
|
|
buffer.Contents = append(buffer.Contents, make(runestring.RuneString, 0))
|
|
}
|
|
|
|
err := os.WriteFile(buffer.filename, []byte(string(buffer.GetContentsAsString())), 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (buffer *Buffer) GetContentsAsString() runestring.RuneString {
|
|
finalText := make(runestring.RuneString, 0)
|
|
for i, line := range buffer.Contents {
|
|
finalText = append(finalText, line...)
|
|
|
|
if i != len(buffer.Contents)-1 {
|
|
finalText = append(finalText, '\n')
|
|
}
|
|
}
|
|
|
|
return finalText
|
|
}
|
|
|
|
func (buffer *Buffer) PositionToAbsolutePosition(position Position) int {
|
|
i := 0
|
|
for lineIndex, line := range buffer.Contents {
|
|
if len(line) == 0 {
|
|
line = append(line, ' ')
|
|
}
|
|
for runeIndex, _ := range append(line, ' ') {
|
|
if position.Equals(runeIndex, lineIndex) {
|
|
return i
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
|
|
return i
|
|
}
|
|
|
|
func (buffer *Buffer) AbsolutePositionToPosition(absolutePosition int) Position {
|
|
i := 0
|
|
|
|
for lineIndex, line := range buffer.Contents {
|
|
for runeIndex, _ := range append(line, ' ') {
|
|
if i == absolutePosition {
|
|
return Position{runeIndex, lineIndex}
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
|
|
lastLine := buffer.Contents[len(buffer.Contents)-1]
|
|
return Position{len(buffer.Contents) - 1, len(lastLine) - 1}
|
|
}
|
|
|
|
func (buffer *Buffer) GetSelectionEdges() (Position, Position) {
|
|
if buffer.Selection == nil {
|
|
return Position{-1, -1}, Position{-1, -1}
|
|
}
|
|
|
|
if ComparePositions(buffer.Selection.selectionStart, buffer.Selection.selectionEnd) == -1 {
|
|
return buffer.Selection.selectionStart, buffer.Selection.selectionEnd
|
|
} else {
|
|
return buffer.Selection.selectionEnd, buffer.Selection.selectionStart
|
|
}
|
|
}
|
|
|
|
func (buffer *Buffer) GetSelectedText() runestring.RuneString {
|
|
if buffer.Selection == nil {
|
|
return make(runestring.RuneString, 0)
|
|
}
|
|
if len(buffer.Contents) == 0 {
|
|
return make(runestring.RuneString, 0)
|
|
}
|
|
|
|
edge1, edge2 := buffer.GetSelectionEdges()
|
|
|
|
selectedText := make(runestring.RuneString, 0)
|
|
if r := buffer.GetCharAtPosition(edge1); r != 0 {
|
|
selectedText = append(selectedText, r)
|
|
}
|
|
|
|
for ComparePositions(edge1, edge2) < 0 {
|
|
edge1.X++
|
|
if edge1.X > len(buffer.Contents[edge1.Y]) {
|
|
if edge1.Y < len(buffer.Contents) {
|
|
edge1.Y++
|
|
edge1.X = 0
|
|
} else {
|
|
edge1.X = len(buffer.Contents[edge1.Y])
|
|
break
|
|
}
|
|
}
|
|
|
|
if r := buffer.GetCharAtPosition(edge1); r != 0 {
|
|
selectedText = append(selectedText, buffer.GetCharAtPosition(edge1))
|
|
}
|
|
}
|
|
|
|
return selectedText
|
|
}
|
|
|
|
func (buffer *Buffer) CutText(window *Window) (runestring.RuneString, int) {
|
|
if buffer.Selection == nil {
|
|
// Cut current line
|
|
cutText := append(buffer.Contents[buffer.CursorPos.Y], '\n')
|
|
|
|
// Remove line from buffer contents
|
|
if len(buffer.Contents) == 1 {
|
|
buffer.Contents[0] = make(runestring.RuneString, 0)
|
|
} else {
|
|
buffer.Contents = slices.Delete(buffer.Contents, buffer.CursorPos.Y, buffer.CursorPos.Y+1)
|
|
}
|
|
|
|
buffer.CursorPos.Y -= 1
|
|
if buffer.CursorPos.Y < 0 {
|
|
buffer.CursorPos = Position{0, 0}
|
|
}
|
|
|
|
return cutText, 0
|
|
} else {
|
|
// Cut selection
|
|
cutText := buffer.GetSelectedText()
|
|
|
|
// Remove selected text
|
|
_, edge2 := buffer.GetSelectionEdges()
|
|
buffer.CursorPos = edge2
|
|
buffer.MoveRight(1)
|
|
|
|
buffer.Delete(len(cutText))
|
|
|
|
// Remove selection
|
|
buffer.Selection = nil
|
|
|
|
return cutText, 1
|
|
}
|
|
}
|
|
|
|
func (buffer *Buffer) CopyText() (runestring.RuneString, int) {
|
|
if buffer.Selection == nil {
|
|
// Cut current line
|
|
copiedText := append(buffer.Contents[buffer.CursorPos.Y], '\n')
|
|
|
|
return copiedText, 0
|
|
} else {
|
|
// Copy selection
|
|
return buffer.GetSelectedText(), 1
|
|
}
|
|
}
|
|
|
|
func (buffer *Buffer) PasteText(window *Window, text runestring.RuneString) {
|
|
// Remove selected text
|
|
if buffer.Selection != nil {
|
|
_, edge2 := buffer.GetSelectionEdges()
|
|
|
|
buffer.CursorPos = edge2
|
|
buffer.Delete(len(buffer.GetSelectedText()))
|
|
|
|
buffer.Selection = nil
|
|
}
|
|
|
|
buffer.WriteString(text)
|
|
}
|
|
|
|
func (buffer *Buffer) FindSubstring(substring runestring.RuneString, afterPos Position) Position {
|
|
// Return no match if afterPos is larger than the buffer contents size
|
|
contents := buffer.GetContentsAsString()
|
|
absAfterPos := buffer.PositionToAbsolutePosition(afterPos)
|
|
|
|
if absAfterPos >= len(contents) {
|
|
return Position{-1, -1}
|
|
}
|
|
|
|
index := runestring.Index(contents[absAfterPos+1:], substring)
|
|
|
|
if index != -1 {
|
|
index += absAfterPos + 1
|
|
}
|
|
return buffer.AbsolutePositionToPosition(index)
|
|
}
|
|
|
|
func (buffer *Buffer) FindAndReplaceSubstring(substring, replacement runestring.RuneString, afterPos Position) Position {
|
|
// Return no match if afterPos is larger than the buffer contents size
|
|
contents := buffer.GetContentsAsString()
|
|
absAfterPos := buffer.PositionToAbsolutePosition(afterPos)
|
|
|
|
if absAfterPos >= len(contents) {
|
|
return Position{-1, -1}
|
|
}
|
|
|
|
index := runestring.Index(contents[absAfterPos+1:], substring)
|
|
|
|
if index != -1 {
|
|
index += absAfterPos + 1
|
|
}
|
|
|
|
// Replace substring with replacement string
|
|
contents = slices.Insert(contents, index, replacement...)
|
|
|
|
buffer.Contents = runestring.Split(contents, '\n')
|
|
|
|
return buffer.AbsolutePositionToPosition(index)
|
|
}
|
|
|
|
func (buffer *Buffer) FindAndReplaceAll(substring, replacement runestring.RuneString) int {
|
|
replacements := 0
|
|
position := Position{}
|
|
for position.X != -1 && position.Y != -1 {
|
|
position = buffer.FindAndReplaceSubstring(substring, replacement, position)
|
|
if position.X != -1 && position.Y != -1 {
|
|
replacements++
|
|
}
|
|
}
|
|
|
|
return replacements
|
|
}
|
|
|
|
func (buffer *Buffer) MoveUp(i int) bool {
|
|
buffer.CursorPos.Y -= i
|
|
if buffer.CursorPos.Y < 0 {
|
|
buffer.CursorPos.Y = 0
|
|
return false
|
|
}
|
|
|
|
if buffer.CursorPos.X >= len(buffer.Contents[buffer.CursorPos.Y]) {
|
|
buffer.CursorPos.X = len(buffer.Contents[buffer.CursorPos.Y])
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (buffer *Buffer) MoveDown(i int) bool {
|
|
buffer.CursorPos.Y += i
|
|
if buffer.CursorPos.Y >= len(buffer.Contents) {
|
|
buffer.CursorPos.Y = len(buffer.Contents) - 1
|
|
return false
|
|
}
|
|
|
|
if buffer.CursorPos.X >= len(buffer.Contents[buffer.CursorPos.Y]) {
|
|
buffer.CursorPos.X = len(buffer.Contents[buffer.CursorPos.Y])
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (buffer *Buffer) MoveLeft(i int) bool {
|
|
remainingSteps := i
|
|
|
|
for remainingSteps > 0 {
|
|
buffer.CursorPos.X--
|
|
if buffer.CursorPos.X < 0 {
|
|
if buffer.CursorPos.Y > 0 {
|
|
buffer.CursorPos.Y--
|
|
buffer.CursorPos.X = len(buffer.Contents[buffer.CursorPos.Y])
|
|
} else {
|
|
buffer.CursorPos.X = 0
|
|
return false
|
|
}
|
|
}
|
|
|
|
remainingSteps--
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (buffer *Buffer) MoveRight(i int) bool {
|
|
remainingSteps := i
|
|
|
|
for remainingSteps > 0 {
|
|
buffer.CursorPos.X++
|
|
if buffer.CursorPos.X > len(buffer.Contents[buffer.CursorPos.Y]) {
|
|
if buffer.CursorPos.Y < len(buffer.Contents)-1 {
|
|
buffer.CursorPos.Y++
|
|
buffer.CursorPos.X = 0
|
|
} else {
|
|
buffer.CursorPos.X = len(buffer.Contents[buffer.CursorPos.Y])
|
|
return false
|
|
}
|
|
}
|
|
|
|
remainingSteps--
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (buffer *Buffer) WriteRune(r rune) {
|
|
if r == '\n' {
|
|
if buffer.CursorPos.Y == len(buffer.Contents) {
|
|
buffer.Contents = append(buffer.Contents, make(runestring.RuneString, 0))
|
|
} else {
|
|
buffer.Contents = slices.Insert(buffer.Contents, buffer.CursorPos.Y+1, make(runestring.RuneString, 0))
|
|
}
|
|
|
|
// Move line content after cursor X to the new line
|
|
line := buffer.Contents[buffer.CursorPos.Y]
|
|
buffer.Contents[buffer.CursorPos.Y+1] = slices.Insert(buffer.Contents[buffer.CursorPos.Y+1], 0, line[buffer.CursorPos.X:]...)
|
|
buffer.Contents[buffer.CursorPos.Y] = line[:buffer.CursorPos.X]
|
|
|
|
buffer.MoveDown(1)
|
|
buffer.CursorPos.X = 0
|
|
} else {
|
|
buffer.Contents[buffer.CursorPos.Y] = slices.Insert(buffer.Contents[buffer.CursorPos.Y], buffer.CursorPos.X, r)
|
|
buffer.MoveRight(1)
|
|
}
|
|
}
|
|
|
|
func (buffer *Buffer) WriteString(str runestring.RuneString) {
|
|
for _, r := range str {
|
|
buffer.WriteRune(r)
|
|
}
|
|
}
|
|
|
|
func (buffer *Buffer) Delete(i int) bool {
|
|
remainingSteps := i
|
|
|
|
for remainingSteps > 0 {
|
|
buffer.CursorPos.X--
|
|
if buffer.CursorPos.X < 0 {
|
|
if buffer.CursorPos.Y > 0 {
|
|
// Save deleted line text
|
|
deletedLine := buffer.Contents[buffer.CursorPos.Y]
|
|
|
|
buffer.CursorPos.Y--
|
|
|
|
// Delete line
|
|
buffer.Contents = slices.Delete(buffer.Contents, buffer.CursorPos.Y+1, buffer.CursorPos.Y+2)
|
|
|
|
// Append deleted line text to end of current line
|
|
buffer.Contents[buffer.CursorPos.Y] = append(buffer.Contents[buffer.CursorPos.Y], deletedLine...)
|
|
|
|
buffer.CursorPos.X = len(buffer.Contents[buffer.CursorPos.Y]) - len(deletedLine)
|
|
} else {
|
|
buffer.CursorPos.X = 0
|
|
return false
|
|
}
|
|
} else {
|
|
buffer.Contents[buffer.CursorPos.Y] = slices.Delete(buffer.Contents[buffer.CursorPos.Y], buffer.CursorPos.X, buffer.CursorPos.X+1)
|
|
}
|
|
|
|
remainingSteps--
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (buffer *Buffer) GetCharAtPosition(position Position) rune {
|
|
if position.Y < 0 || position.Y >= len(buffer.Contents) {
|
|
return 0
|
|
}
|
|
line := buffer.Contents[position.Y]
|
|
|
|
if position.X == len(line) {
|
|
// Do not return newline for last line if it's empty
|
|
if position.Y == len(buffer.Contents)-1 && len(line) == 0 {
|
|
return 0
|
|
}
|
|
|
|
return '\n'
|
|
} else if position.X < 0 || position.X > len(line) {
|
|
return 0
|
|
}
|
|
|
|
return rune(line[position.X])
|
|
}
|
|
|
|
func GetOpenFileBuffer(filename string) *Buffer {
|
|
// Replace tilde with home directory
|
|
if filename != "~" && strings.HasPrefix(filename, "~/") {
|
|
homedir, err := os.UserHomeDir()
|
|
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
filename = filepath.Join(homedir, filename[2:])
|
|
}
|
|
|
|
// Get absolute path of file
|
|
absFilename, err := filepath.Abs(filename)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
for _, buffer := range Buffers {
|
|
if buffer.filename == absFilename {
|
|
return buffer
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func CreateFileBuffer(filename string, openNonExistentFile bool) (*Buffer, error) {
|
|
// Replace tilde with home directory
|
|
if filename != "~" && strings.HasPrefix(filename, "~/") {
|
|
homedir, err := os.UserHomeDir()
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filename = filepath.Join(homedir, filename[2:])
|
|
}
|
|
|
|
// Get absolute path of file
|
|
abs, err := filepath.Abs(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stat, err := os.Stat(abs)
|
|
if !openNonExistentFile {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !stat.Mode().IsRegular() {
|
|
return nil, fmt.Errorf("%s is not a regular file", filename)
|
|
}
|
|
}
|
|
|
|
if GetBufferByName(filename) != nil {
|
|
return nil, fmt.Errorf("a buffer with the name (%s) is already open", filename)
|
|
}
|
|
|
|
if GetBufferByFilename(abs) != nil {
|
|
return nil, fmt.Errorf("%s is already open in another buffer", filename)
|
|
}
|
|
|
|
buffer := Buffer{
|
|
Name: filename,
|
|
Contents: make([]runestring.RuneString, 1),
|
|
CursorPos: Position{0, 0},
|
|
canSave: true,
|
|
canEdit: true,
|
|
filename: abs,
|
|
}
|
|
|
|
// Load file contents if no error was encountered in stat call
|
|
if err == nil {
|
|
err = buffer.Load()
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
Buffers = append(Buffers, &buffer)
|
|
|
|
return &buffer, nil
|
|
}
|
|
|
|
func CreateBuffer(bufferName string) (*Buffer, error) {
|
|
buffer := Buffer{
|
|
Name: bufferName,
|
|
Contents: make([]runestring.RuneString, 1),
|
|
CursorPos: Position{0, 0},
|
|
canSave: true,
|
|
canEdit: true,
|
|
filename: "",
|
|
}
|
|
|
|
if GetBufferByName(bufferName) != nil {
|
|
return nil, fmt.Errorf("a buffer with the name (%s) is already open", bufferName)
|
|
}
|
|
|
|
Buffers = append(Buffers, &buffer)
|
|
|
|
return &buffer, nil
|
|
}
|