diff --git a/internal/action/command.go b/internal/action/command.go index cd97c5222d..d117c0c6da 100644 --- a/internal/action/command.go +++ b/internal/action/command.go @@ -601,6 +601,8 @@ func doSetGlobalOptionNative(option string, nativeValue any) error { } else { config.SetAutoTime(0) } + } else if option == "ambiguouswidth" { + util.SetAmbiguousWidth(nativeValue.(string)) } else if option == "paste" { screen.Screen.SetPaste(nativeValue.(bool)) } else if option == "clipboard" { diff --git a/internal/config/settings.go b/internal/config/settings.go index 45f6b3aad6..a7e7669096 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -21,6 +21,7 @@ type optionValidator func(string, any) error // a list of settings that need option validators var optionValidators = map[string]optionValidator{ + "ambiguouswidth": validateChoice, "autosave": validateNonNegativeValue, "clipboard": validateChoice, "colorcolumn": validateNonNegativeValue, @@ -41,6 +42,7 @@ var optionValidators = map[string]optionValidator{ // a list of settings with pre-defined choices var OptionChoices = map[string][]string{ + "ambiguouswidth": {"auto", "single", "double"}, "clipboard": {"internal", "external", "terminal"}, "fileformat": {"unix", "dos"}, "helpsplit": {"hsplit", "vsplit"}, @@ -110,6 +112,7 @@ var defaultCommonSettings = map[string]any{ // a list of settings that should only be globally modified and their // default values var DefaultGlobalOnlySettings = map[string]any{ + "ambiguouswidth": "auto", "autosave": float64(0), "clipboard": "external", "colorscheme": "default", diff --git a/internal/screen/screen.go b/internal/screen/screen.go index 4b2c564249..86746bbacf 100644 --- a/internal/screen/screen.go +++ b/internal/screen/screen.go @@ -7,6 +7,7 @@ import ( "sync" "github.com/micro-editor/micro/v2/internal/config" + "github.com/micro-editor/micro/v2/internal/util" "github.com/micro-editor/tcell/v2" ) @@ -182,6 +183,8 @@ func TempStart(screenWasNil bool) { func Init() error { drawChan = make(chan bool, 8) + util.SetAmbiguousWidth(config.GetGlobalOption("ambiguouswidth").(string)) + // Should we enable true color? truecolor := config.GetGlobalOption("truecolor").(string) if truecolor == "on" || (truecolor == "auto" && os.Getenv("MICRO_TRUECOLOR") == "1") { diff --git a/internal/util/util.go b/internal/util/util.go index 9838b025fb..d7d02c67f1 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -91,6 +91,31 @@ func init() { } Stdout = new(bytes.Buffer) + + envAmbiguousWide = runewidth.EastAsianWidth +} + +// envAmbiguousWide is the ambiguous width detected from the environment +// (the locale environment variables and RUNEWIDTH_EASTASIAN) at startup +var envAmbiguousWide bool + +// SetAmbiguousWidth sets how wide the characters with the East Asian +// Ambiguous width property (see Unicode Standard Annex #11) are considered +// to be: one column ("single"), two columns ("double") or as many columns +// as the environment implies ("auto") +func SetAmbiguousWidth(mode string) { + var wide bool + switch mode { + case "single": + wide = false + case "double": + wide = true + default: + wide = envAmbiguousWide + } + // go-runewidth syncs DefaultCondition with runewidth.EastAsianWidth only + // once, in its init(), so overriding it later has to be done here + runewidth.DefaultCondition.EastAsianWidth = wide } // SliceEnd returns a byte slice where the index is a rune index diff --git a/internal/util/util_test.go b/internal/util/util_test.go index 454b5c7963..7208f7fbe7 100644 --- a/internal/util/util_test.go +++ b/internal/util/util_test.go @@ -13,6 +13,31 @@ func TestStringWidth(t *testing.T) { assert.Equal(t, 26, n) } +func TestSetAmbiguousWidth(t *testing.T) { + defer SetAmbiguousWidth("auto") + + // U+03B1 is ambiguous width, U+3042 is wide, U+0061 is narrow + bytes := []byte("\u03b1\u3042a") + + SetAmbiguousWidth("single") + assert.Equal(t, 1, StringWidth(bytes, 1, 4)) + assert.Equal(t, 3, StringWidth(bytes, 2, 4)) + assert.Equal(t, 4, StringWidth(bytes, 3, 4)) + + SetAmbiguousWidth("double") + assert.Equal(t, 2, StringWidth(bytes, 1, 4)) + assert.Equal(t, 4, StringWidth(bytes, 2, 4)) + assert.Equal(t, 5, StringWidth(bytes, 3, 4)) + + // "auto" restores the width implied by the environment + SetAmbiguousWidth("auto") + expected := 1 + if envAmbiguousWide { + expected = 2 + } + assert.Equal(t, expected, StringWidth(bytes, 1, 4)) +} + func TestSliceVisualEnd(t *testing.T) { s := []byte("\thello") slc, n, _ := SliceVisualEnd(s, 2, 4) diff --git a/runtime/help/options.md b/runtime/help/options.md index 542089425b..62f5a90ba3 100644 --- a/runtime/help/options.md +++ b/runtime/help/options.md @@ -11,6 +11,23 @@ if you have set either of the above environment variables). Here are the available options: +* `ambiguouswidth`: how wide the characters with the East Asian Ambiguous + width property (as defined by [Unicode Standard Annex + #11](https://www.unicode.org/reports/tr11)) are considered to be. These + characters, e.g. Greek and Cyrillic letters, accented Latin letters and box + drawing characters, are drawn one column wide by most terminals but two + columns wide by some terminals in an East Asian environment. If micro's + choice does not match your terminal, text is misaligned and the cursor is + drawn in the wrong column. This setting is `global only`. + * `auto`: use two columns if the locale environment variables (`LC_ALL`, + `LC_CTYPE`, `LANG`) indicate a Chinese, Japanese or Korean locale, + otherwise one column. Can be overridden with the `RUNEWIDTH_EASTASIAN` + environment variable. + * `single`: always use one column, like `wcwidth(3)` and most terminals do. + * `double`: always use two columns. + + default value: `auto` + * `autoindent`: when creating a new line, use the same indentation as the previous line.