Initial QSfera import
This commit is contained in:
+288
@@ -0,0 +1,288 @@
|
||||
package twwidth
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Tab rune
|
||||
|
||||
const (
|
||||
TabWidthDefault = 8
|
||||
TabString Tab = '\t'
|
||||
)
|
||||
|
||||
// IsTab returns true if t equals the default tab.
|
||||
func (t Tab) IsTab() bool {
|
||||
return t == TabString
|
||||
}
|
||||
|
||||
func (t Tab) Byte() byte {
|
||||
return byte(t)
|
||||
}
|
||||
|
||||
func (t Tab) Rune() rune {
|
||||
return rune(t)
|
||||
}
|
||||
|
||||
func (t Tab) String() string {
|
||||
return string(t)
|
||||
}
|
||||
|
||||
// IsTab returns true if r is a tab rune.
|
||||
func IsTab(r rune) bool {
|
||||
return r == TabString.Rune()
|
||||
}
|
||||
|
||||
type Tabinal struct {
|
||||
once sync.Once
|
||||
width int
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (t *Tabinal) String() string {
|
||||
return TabString.String()
|
||||
}
|
||||
|
||||
// Size returns the current tab width, default if unset.
|
||||
func (t *Tabinal) Size() int {
|
||||
t.once.Do(t.init)
|
||||
|
||||
t.mu.RLock()
|
||||
w := t.width
|
||||
t.mu.RUnlock()
|
||||
|
||||
if w <= 0 {
|
||||
return TabWidthDefault
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// SetWidth sets the tab width if valid (1–32).
|
||||
func (t *Tabinal) SetWidth(w int) {
|
||||
if w <= 0 || w > 32 {
|
||||
return
|
||||
}
|
||||
t.mu.Lock()
|
||||
t.width = w
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
func (t *Tabinal) init() {
|
||||
w := t.detect()
|
||||
t.mu.Lock()
|
||||
t.width = w
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
// detect determines tab width using env, editorconfig, project, or term.
|
||||
func (t *Tabinal) detect() int {
|
||||
if w := envInt("TABWIDTH"); w > 0 {
|
||||
return clamp(w)
|
||||
}
|
||||
if w := envInt("TS"); w > 0 {
|
||||
return clamp(w)
|
||||
}
|
||||
if w := envInt("VIM_TABSTOP"); w > 0 {
|
||||
return clamp(w)
|
||||
}
|
||||
if w := editorConfigTabWidth(); w > 0 {
|
||||
return w
|
||||
}
|
||||
if w := projectHeuristic(); w > 0 {
|
||||
return w
|
||||
}
|
||||
if w := termHeuristic(); w > 0 {
|
||||
return w
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func editorConfigTabWidth() int {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
for dir != "" && dir != "/" && dir != "." {
|
||||
path := filepath.Join(dir, ".editorconfig")
|
||||
if w := parseEditorConfig(path); w > 0 {
|
||||
return clamp(w)
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// parseEditorConfig reads tab_width or indent_size from a file.
|
||||
func parseEditorConfig(path string) int {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
inMatch := false
|
||||
globalWidth := 0
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
pattern := line[1 : len(line)-1]
|
||||
inMatch = pattern == "*"
|
||||
|
||||
knownExts := []string{".go", ".py", ".js", ".ts", ".java", ".rs"}
|
||||
for _, ext := range knownExts {
|
||||
if strings.Contains(pattern, ext) {
|
||||
inMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !inMatch && globalWidth == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
val := strings.TrimSpace(parts[1])
|
||||
|
||||
switch key {
|
||||
case "tab_width":
|
||||
if w, err := strconv.Atoi(val); err == nil && w > 0 {
|
||||
if inMatch {
|
||||
return clamp(w)
|
||||
}
|
||||
if globalWidth == 0 {
|
||||
globalWidth = w
|
||||
}
|
||||
}
|
||||
case "indent_size":
|
||||
if val == "tab" {
|
||||
continue
|
||||
}
|
||||
if w, err := strconv.Atoi(val); err == nil && w > 0 {
|
||||
if inMatch {
|
||||
return clamp(w)
|
||||
}
|
||||
if globalWidth == 0 {
|
||||
globalWidth = w
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return globalWidth
|
||||
}
|
||||
|
||||
// projectHeuristic returns 4 for known project types.
|
||||
func projectHeuristic() int {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
indicators := []string{
|
||||
"go.mod", "go.sum",
|
||||
"package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
|
||||
"setup.py", "pyproject.toml", "requirements.txt", "Pipfile",
|
||||
"pom.xml", "build.gradle", "build.gradle.kts",
|
||||
"Cargo.toml",
|
||||
"composer.json",
|
||||
}
|
||||
|
||||
for _, indicator := range indicators {
|
||||
if _, err := os.Stat(filepath.Join(dir, indicator)); err == nil {
|
||||
return 4
|
||||
}
|
||||
}
|
||||
|
||||
patterns := []string{"*.go", "*.py", "*.js", "*.ts", "*.java", "*.rs"}
|
||||
for _, pattern := range patterns {
|
||||
if matches, _ := filepath.Glob(filepath.Join(dir, pattern)); len(matches) > 0 {
|
||||
return 4
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// termHeuristic returns a default width based on the TERM variable.
|
||||
func termHeuristic() int {
|
||||
termEnv := strings.ToLower(os.Getenv("TERM"))
|
||||
if termEnv == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
if strings.Contains(termEnv, "vt52") {
|
||||
return 2
|
||||
}
|
||||
|
||||
if strings.Contains(termEnv, "xterm") ||
|
||||
strings.Contains(termEnv, "screen") ||
|
||||
strings.Contains(termEnv, "tmux") ||
|
||||
strings.Contains(termEnv, "linux") ||
|
||||
strings.Contains(termEnv, "ansi") ||
|
||||
strings.Contains(termEnv, "rxvt") {
|
||||
return TabWidthDefault
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func clamp(w int) int {
|
||||
if w <= 0 {
|
||||
return 0
|
||||
}
|
||||
if w > 32 {
|
||||
return 32
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
var (
|
||||
globalTab *Tabinal
|
||||
globalTabOnce sync.Once
|
||||
)
|
||||
|
||||
// TabInstance returns the singleton Tabinal.
|
||||
func TabInstance() *Tabinal {
|
||||
globalTabOnce.Do(func() {
|
||||
globalTab = &Tabinal{}
|
||||
})
|
||||
return globalTab
|
||||
}
|
||||
|
||||
// TabWidth returns the detected global tab width.
|
||||
func TabWidth() int {
|
||||
return TabInstance().Size()
|
||||
}
|
||||
|
||||
// SetTabWidth sets the global tab width.
|
||||
func SetTabWidth(w int) {
|
||||
TabInstance().SetWidth(w)
|
||||
}
|
||||
|
||||
func envInt(k string) int {
|
||||
v := os.Getenv(k)
|
||||
w, _ := strconv.Atoi(v)
|
||||
return w
|
||||
}
|
||||
Reference in New Issue
Block a user