turbo-editors/turbo-pythonpublic Fork 0
v1.0.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-python.git
git clone ssh://git@rickub.com/turbo-editors/turbo-python.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

📦 Turbo Python 6fc62ea · on v1.0.0 · k33g · 9h ago
literals.go · 129 lines · 4.2 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package pythonlang

// The strings of Python: eight prefixes, two quotes, and each of those in a
// single and a triple form — sixteen spellings of one construct, plus the two
// different ways one of them reaches the next line.

import (
	"strings"

	"rickub.com/turbo-editors/turbo-core/syntax"
)

// maxStringPrefix is how many letters may come before the quote: rb, fr and
// their case variants are two, and nothing in the language is three.
const maxStringPrefix = 2

// isStringStart reports whether a string literal opens at the scanner's
// position, prefix included.
func isStringStart(s *syntax.LineScanner) bool {
	_, opens := stringPrefixLength(s)
	return opens
}

// stringPrefixLength returns how many prefix letters come before the quote, and
// whether a string opens here at all.
//
// It accepts any one or two of f, r, b and u in any order and in any case,
// which is a little more generous than the language: `bu"…"` is not a Python
// string and is coloured as one. Being generous is the right way to be wrong
// here — the alternative is a table of the twenty-four accepted spellings, and
// a half-typed prefix under the cursor colouring as an identifier and a string
// that are not there.
func stringPrefixLength(s *syntax.LineScanner) (int, bool) {
	for length := 0; length <= maxStringPrefix; length++ {
		switch r := s.Peek(length); {
		case r == '"' || r == '\'':
			return length, true
		case !isStringPrefixRune(r):
			return 0, false
		}
	}
	return 0, false
}

// isStringPrefixRune reports whether a rune may appear in a string's prefix.
func isStringPrefixRune(r rune) bool {
	switch r {
	case 'f', 'F', 'r', 'R', 'b', 'B', 'u', 'U':
		return true
	}
	return false
}

// takeString colours a string from its prefix, remembering what would close it.
//
// An f-string's {expression} is *not* scanned as code. Since Python 3.12 it may
// hold anything, nested quotes and comments included, so colouring it properly
// means running the whole scanner inside itself; colouring it half-properly
// means a brace in a format spec — "{n:{width}}" — ending the string early. One
// flat run is the honest answer, and it is the one this scanner gives.
func takeString(s *syntax.LineScanner, open *carry) {
	start := s.Pos()

	prefix, _ := stringPrefixLength(s)
	s.Advance(prefix)

	quote := s.Peek(0)
	triple := s.Peek(1) == quote && s.Peek(2) == quote
	if triple {
		s.Advance(3)
	} else {
		s.Advance(1)
	}
	open.open, open.quote, open.triple = true, quote, triple

	consumeString(s, open)
	s.Emit(start, s.Pos(), syntax.ClassString)
}

// continueString colours the rest of a string opened on an earlier line, and
// reports whether the line has code after it.
func continueString(s *syntax.LineScanner, open *carry) bool {
	consumeString(s, open)
	s.Emit(0, s.Pos(), syntax.ClassString)
	return !open.open && !s.AtEnd()
}

// consumeString runs to whatever closes the string, or to the end of the line,
// and decides there whether the string carries on to the next one.
//
// A backslash takes the rune after it out of consideration, in a raw string as
// much as in an ordinary one — that is what makes r"\"" one string rather than
// two. A backslash that is itself the last rune on the line escapes the newline
// instead, which is the only way a single-quoted string reaches the next line.
func consumeString(s *syntax.LineScanner, open *carry) {
	closer := stringCloser(*open)
	continued := false

	for !s.AtEnd() {
		switch {
		case s.Peek(0) == '\\':
			continued = s.Pos()+2 > s.Len()
			s.Advance(2)
		case s.HasPrefix(0, closer):
			s.Advance(len([]rune(closer)))
			open.open = false
			return
		default:
			continued = false
			s.Advance(1)
		}
	}

	// The line ended with the string still open. A triple-quoted one is meant
	// to do that; a single-quoted one only does it when the newline was
	// escaped. Anything else is source in the middle of being typed, and is
	// left behind rather than carried into the rest of the file.
	if !open.triple && !continued {
		open.open = false
	}
}

// stringCloser returns the text that ends the string being scanned.
func stringCloser(open carry) string {
	if open.triple {
		return strings.Repeat(string(open.quote), 3)
	}
	return string(open.quote)
}