From 2383574d62328684eec17958f521be7a0a6badb4 Mon Sep 17 00:00:00 2001 From: upstream Date: Wed, 7 Oct 2026 20:08:48 +0200 Subject: [PATCH] Upstream olekukonko/tablewriter state of the touched files before c715bafde1 --- pkg/twwidth/tab.go | 289 ++++++++++++++++++++++++++++++++++++++++ pkg/twwidth/tab_test.go | 40 ++++++ 2 files changed, 329 insertions(+) create mode 100644 pkg/twwidth/tab.go create mode 100644 pkg/twwidth/tab_test.go diff --git a/pkg/twwidth/tab.go b/pkg/twwidth/tab.go new file mode 100644 index 0000000..f309c0b --- /dev/null +++ b/pkg/twwidth/tab.go @@ -0,0 +1,289 @@ +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.once.Do(t.init) + 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 +} diff --git a/pkg/twwidth/tab_test.go b/pkg/twwidth/tab_test.go new file mode 100644 index 0000000..b02bb57 --- /dev/null +++ b/pkg/twwidth/tab_test.go @@ -0,0 +1,40 @@ +package twwidth_test + +import ( + "fmt" + "testing" + + "github.com/olekukonko/tablewriter/pkg/twwidth" +) + +func TestTabinalSetWidth(t *testing.T) { + t.Setenv("TABWIDTH", "3") + + for _, initialized := range []bool{false, true} { + for _, width := range []int{1, 2, 32, 0, -1, 33} { + t.Run(fmt.Sprintf("initialized=%v/width=%d", initialized, width), func(t *testing.T) { + var tab twwidth.Tabinal + if initialized { + if got := tab.Size(); got != 3 { + t.Fatalf("initial Size() = %d, want 3", got) + } + } + + tab.SetWidth(width) + want := width + if width <= 0 || width > 32 { + want = 3 + } + if got := tab.Size(); got != want { + t.Fatalf("Size() after SetWidth(%d) = %d, want %d", width, got, want) + } + + tab.SetWidth(5) + tab.SetWidth(0) + if got := tab.Size(); got != 5 { + t.Fatalf("Size() after changing width = %d, want 5", got) + } + }) + } + } +}