Move the kittens Go code into the kittens folder

This commit is contained in:
Kovid Goyal
2023-03-27 13:06:02 +05:30
parent 3f9579d61d
commit ff55121094
46 changed files with 17 additions and 21 deletions

327
kittens/hints/main.go Normal file
View File

@@ -0,0 +1,327 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package hints
import (
"fmt"
"io"
"os"
"strconv"
"strings"
"unicode"
"kitty/tools/cli"
"kitty/tools/tty"
"kitty/tools/tui"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/utils/style"
"kitty/tools/wcswidth"
)
var _ = fmt.Print
func convert_text(text string, cols int) string {
lines := make([]string, 0, 64)
empty_line := strings.Repeat("\x00", cols) + "\n"
s1 := utils.NewLineScanner(text)
for s1.Scan() {
full_line := s1.Text()
if full_line == "" {
continue
}
if strings.TrimRight(full_line, "\r") == "" {
for i := 0; i < len(full_line); i++ {
lines = append(lines, empty_line)
}
continue
}
appended := false
s2 := utils.NewSeparatorScanner(full_line, "\r")
for s2.Scan() {
line := s2.Text()
if line != "" {
line_sz := wcswidth.Stringwidth(line)
extra := cols - line_sz
if extra > 0 {
line += strings.Repeat("\x00", extra)
}
lines = append(lines, line)
lines = append(lines, "\r")
appended = true
}
}
if appended {
lines[len(lines)-1] = "\n"
}
}
ans := strings.Join(lines, "")
return strings.TrimRight(ans, "\r\n")
}
func parse_input(text string) string {
cols, err := strconv.Atoi(os.Getenv("OVERLAID_WINDOW_COLS"))
if err == nil {
return convert_text(text, cols)
}
term, err := tty.OpenControllingTerm()
if err == nil {
sz, err := term.GetSize()
term.Close()
if err == nil {
return convert_text(text, int(sz.Col))
}
}
return convert_text(text, 80)
}
type Result struct {
Match []string `json:"match"`
Programs []string `json:"programs"`
Multiple_joiner string `json:"multiple_joiner"`
Customize_processing string `json:"customize_processing"`
Type string `json:"type"`
Groupdicts []map[string]any `json:"groupdicts"`
Extra_cli_args []string `json:"extra_cli_args"`
Linenum_action string `json:"linenum_action"`
Cwd string `json:"cwd"`
}
func encode_hint(num int, alphabet string) (res string) {
runes := []rune(alphabet)
d := len(runes)
for res == "" || num > 0 {
res = string(runes[num%d]) + res
num /= d
}
return
}
func decode_hint(x string, alphabet string) (ans int) {
base := len(alphabet)
index_map := make(map[rune]int, len(alphabet))
for i, c := range alphabet {
index_map[c] = i
}
for _, char := range x {
ans = ans*base + index_map[char]
}
return
}
func main(_ *cli.Command, o *Options, args []string) (rc int, err error) {
output := tui.KittenOutputSerializer()
if tty.IsTerminal(os.Stdin.Fd()) {
tui.ReportError(fmt.Errorf("You must pass the text to be hinted on STDIN"))
return 1, nil
}
stdin, err := io.ReadAll(os.Stdin)
if err != nil {
tui.ReportError(fmt.Errorf("Failed to read from STDIN with error: %w", err))
return 1, nil
}
if len(args) > 0 && o.CustomizeProcessing == "" && o.Type != "linenum" {
tui.ReportError(fmt.Errorf("Extra command line arguments present: %s", strings.Join(args, " ")))
return 1, nil
}
input_text := parse_input(utils.UnsafeBytesToString(stdin))
text, all_marks, index_map, err := find_marks(input_text, o, os.Args[2:]...)
if err != nil {
tui.ReportError(err)
return 1, nil
}
result := Result{
Programs: o.Program, Multiple_joiner: o.MultipleJoiner, Customize_processing: o.CustomizeProcessing, Type: o.Type,
Extra_cli_args: args, Linenum_action: o.LinenumAction,
}
result.Cwd, _ = os.Getwd()
alphabet := o.Alphabet
if alphabet == "" {
alphabet = DEFAULT_HINT_ALPHABET
}
ignore_mark_indices := utils.NewSet[int](8)
window_title := o.WindowTitle
if window_title == "" {
switch o.Type {
case "url":
window_title = "Choose URL"
default:
window_title = "Choose text"
}
}
current_text := ""
current_input := ""
match_suffix := ""
switch o.AddTrailingSpace {
case "always":
match_suffix = " "
case "never":
default:
if o.Multiple {
match_suffix = " "
}
}
chosen := []*Mark{}
lp, err := loop.New(loop.NoAlternateScreen) // no alternate screen reduces flicker on exit
if err != nil {
return
}
fctx := style.Context{AllowEscapeCodes: true}
faint := fctx.SprintFunc("dim")
hint_style := fctx.SprintFunc(fmt.Sprintf("fg=%s bg=%s bold", o.HintsForegroundColor, o.HintsBackgroundColor))
text_style := fctx.SprintFunc(fmt.Sprintf("fg=bright-%s bold", o.HintsTextColor))
highlight_mark := func(m *Mark, mark_text string) string {
hint := encode_hint(m.Index, alphabet)
if current_input != "" && !strings.HasPrefix(hint, current_input) {
return faint(mark_text)
}
hint = hint[len(current_input):]
if hint == "" {
hint = " "
}
mark_text = mark_text[len(hint):]
return hint_style(hint) + text_style(mark_text)
}
render := func() string {
ans := text
for i := len(all_marks) - 1; i >= 0; i-- {
mark := &all_marks[i]
if ignore_mark_indices.Has(mark.Index) {
continue
}
mtext := highlight_mark(mark, ans[mark.Start:mark.End])
ans = ans[:mark.Start] + mtext + ans[mark.End:]
}
ans = strings.ReplaceAll(ans, "\x00", "")
return strings.TrimRightFunc(strings.NewReplacer("\r", "\r\n", "\n", "\r\n").Replace(ans), unicode.IsSpace)
}
draw_screen := func() {
lp.StartAtomicUpdate()
defer lp.EndAtomicUpdate()
if current_text == "" {
current_text = render()
}
lp.ClearScreen()
lp.QueueWriteString(current_text)
}
reset := func() {
current_input = ""
current_text = ""
}
lp.OnInitialize = func() (string, error) {
lp.SendOverlayReady()
lp.SetCursorVisible(false)
lp.SetWindowTitle(window_title)
lp.AllowLineWrapping(false)
draw_screen()
return "", nil
}
lp.OnFinalize = func() string {
lp.SetCursorVisible(true)
return ""
}
lp.OnResize = func(old_size, new_size loop.ScreenSize) error {
draw_screen()
return nil
}
lp.OnText = func(text string, _, _ bool) error {
changed := false
for _, ch := range text {
if strings.ContainsRune(alphabet, ch) {
current_input += string(ch)
changed = true
}
}
if changed {
matches := []*Mark{}
for idx, m := range index_map {
if eh := encode_hint(idx, alphabet); strings.HasPrefix(eh, current_input) {
matches = append(matches, m)
}
}
if len(matches) == 1 {
chosen = append(chosen, matches[0])
if o.Multiple {
ignore_mark_indices.Add(matches[0].Index)
reset()
} else {
lp.Quit(0)
return nil
}
}
current_text = ""
draw_screen()
}
return nil
}
lp.OnKeyEvent = func(ev *loop.KeyEvent) error {
if ev.MatchesPressOrRepeat("backspace") {
ev.Handled = true
r := []rune(current_input)
if len(r) > 0 {
r = r[:len(r)-1]
current_input = string(r)
current_text = ""
}
draw_screen()
} else if ev.MatchesPressOrRepeat("enter") || ev.MatchesPressOrRepeat("space") {
ev.Handled = true
if current_input != "" {
idx := decode_hint(current_input, alphabet)
if m := index_map[idx]; m != nil {
chosen = append(chosen, m)
ignore_mark_indices.Add(idx)
if o.Multiple {
reset()
draw_screen()
} else {
lp.Quit(0)
}
} else {
current_input = ""
current_text = ""
draw_screen()
}
}
} else if ev.MatchesPressOrRepeat("esc") {
if o.Multiple {
lp.Quit(0)
} else {
lp.Quit(1)
}
}
return nil
}
err = lp.Run()
if err != nil {
return 1, err
}
ds := lp.DeathSignalName()
if ds != "" {
fmt.Println("Killed by signal: ", ds)
lp.KillIfSignalled()
return 1, nil
}
if lp.ExitCode() != 0 {
return lp.ExitCode(), nil
}
result.Match = make([]string, len(chosen))
result.Groupdicts = make([]map[string]any, len(chosen))
for i, m := range chosen {
result.Match[i] = m.Text + match_suffix
result.Groupdicts[i] = m.Groupdict
}
fmt.Println(output(result))
return
}
func EntryPoint(parent *cli.Command) {
create_cmd(parent, main)
}

419
kittens/hints/marks.go Normal file
View File

@@ -0,0 +1,419 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package hints
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"kitty"
"kitty/tools/config"
"kitty/tools/utils"
"os/exec"
"path/filepath"
"regexp"
"strings"
"unicode/utf8"
"github.com/seancfoley/ipaddress-go/ipaddr"
"golang.org/x/exp/slices"
)
var _ = fmt.Print
const (
DEFAULT_HINT_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"
FILE_EXTENSION = `\.(?:[a-zA-Z0-9]{2,7}|[ahcmo])(?:\b|[^.])`
)
func path_regex() string {
return fmt.Sprintf(`(?:\S*?/[\r\S]+)|(?:\S[\r\S]*%s)\b`, FILE_EXTENSION)
}
func default_linenum_regex() string {
return fmt.Sprintf(`(?P<path>%s):(?P<line>\d+)`, path_regex())
}
type Mark struct {
Index int `json:"index"`
Start int `json:"start"`
End int `json:"end"`
Text string `json:"text"`
Group_id string `json:"group_id"`
Is_hyperlink bool `json:"is_hyperlink"`
Groupdict map[string]any `json:"groupdict"`
}
func process_escape_codes(text string) (ans string, hyperlinks []Mark) {
removed_size, idx := 0, 0
active_hyperlink_url := ""
active_hyperlink_id := ""
active_hyperlink_start_offset := 0
add_hyperlink := func(end int) {
hyperlinks = append(hyperlinks, Mark{
Index: idx, Start: active_hyperlink_start_offset, End: end, Text: active_hyperlink_url, Is_hyperlink: true, Group_id: active_hyperlink_id})
active_hyperlink_url, active_hyperlink_id = "", ""
active_hyperlink_start_offset = 0
idx++
}
ans = utils.ReplaceAll(utils.MustCompile("\x1b(?:\\[[0-9;:]*?m|\\].*?\x1b\\\\)"), text, func(raw string, groupdict map[string]utils.SubMatch) string {
if !strings.HasPrefix(raw, "\x1b]8") {
removed_size += len(raw)
return ""
}
start := groupdict[""].Start - removed_size
removed_size += len(raw)
if active_hyperlink_url != "" {
add_hyperlink(start)
}
raw = raw[4 : len(raw)-2]
if metadata, url, found := strings.Cut(raw, ";"); found && url != "" {
active_hyperlink_url = url
active_hyperlink_start_offset = start
if metadata != "" {
for _, entry := range strings.Split(metadata, ":") {
if strings.HasPrefix(entry, "id=") && len(entry) > 3 {
active_hyperlink_id = entry[3:]
}
}
}
}
return ""
})
if active_hyperlink_url != "" {
add_hyperlink(len(ans))
}
return
}
type PostProcessorFunc = func(string, int, int) (int, int)
type GroupProcessorFunc = func(map[string]string)
func is_punctuation(b string) bool {
switch b {
case ",", ".", "?", "!":
return true
}
return false
}
func closing_bracket_for(ch string) string {
switch ch {
case "(":
return ")"
case "[":
return "]"
case "{":
return "}"
case "<":
return ">"
case "*":
return "*"
case `"`:
return `"`
case "'":
return "'"
case "“":
return "”"
case "":
return ""
}
return ""
}
func char_at(s string, i int) string {
ans, _ := utf8.DecodeRuneInString(s[i:])
if ans == utf8.RuneError {
return ""
}
return string(ans)
}
func matching_remover(openers ...string) PostProcessorFunc {
return func(text string, s, e int) (int, int) {
if s < e && e <= len(text) {
before := char_at(text, s)
if slices.Index(openers, before) > -1 {
q := closing_bracket_for(before)
if e > 0 && char_at(text, e-1) == q {
s++
e--
} else if char_at(text, e) == q {
s++
}
}
}
return s, e
}
}
func linenum_group_processor(gd map[string]string) {
pat := utils.MustCompile(`:\d+$`)
gd[`path`] = pat.ReplaceAllStringFunc(gd["path"], func(m string) string {
gd["line"] = m[1:]
return ``
})
gd[`path`] = utils.Expanduser(gd[`path`])
}
var PostProcessorMap = (&utils.Once[map[string]PostProcessorFunc]{Run: func() map[string]PostProcessorFunc {
return map[string]PostProcessorFunc{
"url": func(text string, s, e int) (int, int) {
if s > 4 && text[s-5:s] == "link:" { // asciidoc URLs
url := text[s:e]
idx := strings.LastIndex(url, "[")
if idx > -1 {
e -= len(url) - idx
}
}
for e > 1 && is_punctuation(char_at(text, e)) { // remove trailing punctuation
e--
}
// truncate url at closing bracket/quote
if s > 0 && e <= len(text) && closing_bracket_for(char_at(text, s-1)) != "" {
q := closing_bracket_for(char_at(text, s-1))
idx := strings.Index(text[s:], q)
if idx > 0 {
e = s + idx
}
}
// reStructuredText URLs
if e > 3 && text[e-2:e] == "`_" {
e -= 2
}
return s, e
},
"brackets": matching_remover("(", "{", "[", "<"),
"quotes": matching_remover("'", `"`, "“", ""),
"ip": func(text string, s, e int) (int, int) {
addr := ipaddr.NewHostName(text[s:e])
if !addr.IsAddress() {
return -1, -1
}
return s, e
},
}
}}).Get
type KittyOpts struct {
Url_prefixes *utils.Set[string]
Select_by_word_characters string
}
func read_relevant_kitty_opts(path string) KittyOpts {
ans := KittyOpts{Select_by_word_characters: kitty.KittyConfigDefaults.Select_by_word_characters}
handle_line := func(key, val string) error {
switch key {
case "url_prefixes":
ans.Url_prefixes = utils.NewSetWithItems(strings.Split(val, " ")...)
case "select_by_word_characters":
ans.Select_by_word_characters = strings.TrimSpace(val)
}
return nil
}
cp := config.ConfigParser{LineHandler: handle_line}
cp.ParseFiles(path)
if ans.Url_prefixes == nil {
ans.Url_prefixes = utils.NewSetWithItems(kitty.KittyConfigDefaults.Url_prefixes...)
}
return ans
}
var RelevantKittyOpts = (&utils.Once[KittyOpts]{Run: func() KittyOpts {
return read_relevant_kitty_opts(filepath.Join(utils.ConfigDir(), "kitty.conf"))
}}).Get
func functions_for(opts *Options) (pattern string, post_processors []PostProcessorFunc, group_processors []GroupProcessorFunc) {
switch opts.Type {
case "url":
var url_prefixes *utils.Set[string]
if opts.UrlPrefixes == "default" {
url_prefixes = RelevantKittyOpts().Url_prefixes
} else {
url_prefixes = utils.NewSetWithItems(strings.Split(opts.UrlPrefixes, ",")...)
}
pattern = fmt.Sprintf(`(?:%s)://[^%s]{3,}`, strings.Join(url_prefixes.AsSlice(), "|"), URL_DELIMITERS)
post_processors = append(post_processors, PostProcessorMap()["url"])
case "path":
pattern = path_regex()
post_processors = append(post_processors, PostProcessorMap()["brackets"], PostProcessorMap()["quotes"])
case "line":
pattern = "(?m)^\\s*(.+)[\\s\x00]*$"
case "hash":
pattern = "[0-9a-f][0-9a-f\r]{6,127}"
case "ip":
pattern = (
// IPv4 with no validation
`((?:\d{1,3}\.){3}\d{1,3}` + "|" +
// IPv6 with no validation
`(?:[a-fA-F0-9]{0,4}:){2,7}[a-fA-F0-9]{1,4})`)
post_processors = append(post_processors, PostProcessorMap()["ip"])
case "word":
chars := opts.WordCharacters
if chars == "" {
chars = RelevantKittyOpts().Select_by_word_characters
}
chars = regexp.QuoteMeta(chars)
chars = strings.ReplaceAll(chars, "-", "\\-")
pattern = fmt.Sprintf(`[%s\pL\pN]{%d,}`, chars, opts.MinimumMatchLength)
post_processors = append(post_processors, PostProcessorMap()["brackets"], PostProcessorMap()["quotes"])
default:
pattern = opts.Regex
if opts.Type == "linenum" {
if pattern == kitty.HintsDefaultRegex {
pattern = default_linenum_regex()
}
post_processors = append(post_processors, PostProcessorMap()["brackets"], PostProcessorMap()["quotes"])
group_processors = append(group_processors, linenum_group_processor)
}
}
return
}
func mark(r *regexp.Regexp, post_processors []PostProcessorFunc, group_processors []GroupProcessorFunc, text string, opts *Options) (ans []Mark) {
sanitize_pat := regexp.MustCompile("[\r\n\x00]")
names := r.SubexpNames()
for i, v := range r.FindAllStringSubmatchIndex(text, -1) {
match_start, match_end := v[0], v[1]
for match_end > match_start+1 && text[match_end-1] == 0 {
match_end--
}
full_match := text[match_start:match_end]
if len([]rune(full_match)) < opts.MinimumMatchLength {
continue
}
for _, f := range post_processors {
match_start, match_end = f(text, match_start, match_end)
if match_start < 0 {
break
}
}
if match_start < 0 {
continue
}
full_match = sanitize_pat.ReplaceAllLiteralString(text[match_start:match_end], "")
gd := make(map[string]string, len(names))
for x, name := range names {
if name != "" {
idx := 2 * x
if s, e := v[idx], v[idx+1]; s > -1 && e > -1 {
s = utils.Max(s, match_start)
e = utils.Min(e, match_end)
gd[name] = sanitize_pat.ReplaceAllLiteralString(text[s:e], "")
}
}
}
for _, f := range group_processors {
f(gd)
}
gd2 := make(map[string]any, len(gd))
for k, v := range gd {
gd2[k] = v
}
ans = append(ans, Mark{
Index: i, Start: match_start, End: match_end, Text: full_match, Groupdict: gd2,
})
}
return
}
type ErrNoMatches struct{ Type string }
func adjust_python_offsets(text string, marks []Mark) error {
// python returns rune based offsets (unicode chars not utf-8 bytes)
adjust := utils.RuneOffsetsToByteOffsets(text)
for i := range marks {
mark := &marks[i]
if mark.End < mark.Start {
return fmt.Errorf("The end of a mark must not be before its start")
}
s, e := adjust(mark.Start), adjust(mark.End)
if s < 0 || e < 0 {
return fmt.Errorf("Overlapping marks are not supported")
}
mark.Start, mark.End = s, e
}
return nil
}
func (self *ErrNoMatches) Error() string {
none_of := "matches"
switch self.Type {
case "urls":
none_of = "URLs"
case "hyperlinks":
none_of = "hyperlinks"
}
return fmt.Sprintf("No %s found", none_of)
}
func find_marks(text string, opts *Options, cli_args ...string) (sanitized_text string, ans []Mark, index_map map[int]*Mark, err error) {
sanitized_text, hyperlinks := process_escape_codes(text)
run_basic_matching := func() error {
pattern, post_processors, group_processors := functions_for(opts)
r, err := regexp.Compile(pattern)
if err != nil {
return fmt.Errorf("Failed to compile the regex pattern: %#v with error: %w", pattern, err)
}
ans = mark(r, post_processors, group_processors, sanitized_text, opts)
return nil
}
if opts.CustomizeProcessing != "" {
cmd := exec.Command(utils.KittyExe(), append([]string{"+runpy", "from kittens.hints.main import custom_marking; custom_marking()"}, cli_args...)...)
cmd.Stdin = strings.NewReader(sanitized_text)
stdout, stderr := bytes.Buffer{}, bytes.Buffer{}
cmd.Stdout, cmd.Stderr = &stdout, &stderr
err = cmd.Run()
if err != nil {
var e *exec.ExitError
if errors.As(err, &e) && e.ExitCode() == 2 {
err = run_basic_matching()
if err != nil {
return
}
goto process_answer
} else {
return "", nil, nil, fmt.Errorf("Failed to run custom processor %#v with error: %w\n%s", opts.CustomizeProcessing, err, stderr.String())
}
}
ans = make([]Mark, 0, 32)
err = json.Unmarshal(stdout.Bytes(), &ans)
if err != nil {
return "", nil, nil, fmt.Errorf("Failed to load output from custom processor %#v with error: %w", opts.CustomizeProcessing, err)
}
err = adjust_python_offsets(sanitized_text, ans)
if err != nil {
return "", nil, nil, fmt.Errorf("Custom processor %#v produced invalid mark output with error: %w", opts.CustomizeProcessing, err)
}
} else if opts.Type == "hyperlink" {
ans = hyperlinks
} else {
err = run_basic_matching()
if err != nil {
return
}
}
process_answer:
if len(ans) == 0 {
return "", nil, nil, &ErrNoMatches{Type: opts.Type}
}
largest_index := ans[len(ans)-1].Index
offset := utils.Max(0, opts.HintsOffset)
index_map = make(map[int]*Mark, len(ans))
for i := range ans {
m := &ans[i]
if opts.Ascending {
m.Index += offset
} else {
m.Index = largest_index - m.Index + offset
}
index_map[m.Index] = m
}
return
}

131
kittens/hints/marks_test.go Normal file
View File

@@ -0,0 +1,131 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package hints
import (
"errors"
"fmt"
"kitty"
"kitty/tools/utils"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
)
var _ = fmt.Print
func TestHintMarking(t *testing.T) {
var opts *Options
cols := 20
cli_args := []string{}
reset := func() {
opts = &Options{Type: "url", UrlPrefixes: "default", Regex: kitty.HintsDefaultRegex}
cols = 20
cli_args = []string{}
}
r := func(text string, url ...string) (marks []Mark) {
ptext := convert_text(text, cols)
ptext, marks, _, err := find_marks(ptext, opts, cli_args...)
if err != nil {
var e *ErrNoMatches
if len(url) != 0 || !errors.As(err, &e) {
t.Fatalf("%#v failed with error: %s", text, err)
}
return
}
actual := utils.Map(func(m Mark) string { return m.Text }, marks)
if diff := cmp.Diff(url, actual); diff != "" {
t.Fatalf("%#v failed:\n%s", text, diff)
}
for _, m := range marks {
q := strings.NewReplacer("\n", "", "\r", "", "\x00", "").Replace(ptext[m.Start:m.End])
if diff := cmp.Diff(m.Text, q); diff != "" {
t.Fatalf("Mark start and end dont point to correct offset in text for %#v\n%s", text, diff)
}
}
return
}
reset()
u := `http://test.me/`
r(u, u)
r(`"`+u+`"`, u)
r("("+u+")", u)
cols = len(u)
r(u+"\nxxx", u+"xxx")
cols = 20
r("link:"+u+"[xxx]", u)
r("`xyz <"+u+">`_.", u)
r(`<a href="`+u+`">moo`, u)
r("\x1b[mhttp://test.me/1234\n\x1b[mx", "http://test.me/1234")
r("\x1b[mhttp://test.me/12345\r\x1b[m6\n\x1b[mx", "http://test.me/123456")
opts.Type = "linenum"
m := func(text, path string, line int) {
ptext := convert_text(text, cols)
_, marks, _, err := find_marks(ptext, opts, cli_args...)
if err != nil {
t.Fatalf("%#v failed with error: %s", text, err)
}
gd := map[string]any{"path": path, "line": strconv.Itoa(line)}
if diff := cmp.Diff(marks[0].Groupdict, gd); diff != "" {
t.Fatalf("%#v failed:\n%s", text, diff)
}
}
m("file.c:23", "file.c", 23)
m("file.c:23:32", "file.c", 23)
m("file.cpp:23:1", "file.cpp", 23)
m("a/file.c:23", "a/file.c", 23)
m("a/file.c:23:32", "a/file.c", 23)
m("~/file.c:23:32", utils.Expanduser("~/file.c"), 23)
reset()
opts.Type = "path"
r("file.c", "file.c")
r("file.c.", "file.c")
r("file.epub.", "file.epub")
r("(file.epub)", "file.epub")
r("some/path", "some/path")
reset()
cols = 60
opts.Type = "ip"
r(`100.64.0.0`, `100.64.0.0`)
r(`2001:0db8:0000:0000:0000:ff00:0042:8329`, `2001:0db8:0000:0000:0000:ff00:0042:8329`)
r(`2001:db8:0:0:0:ff00:42:8329`, `2001:db8:0:0:0:ff00:42:8329`)
r(`2001:db8::ff00:42:8329`, `2001:db8::ff00:42:8329`)
r(`2001:DB8::FF00:42:8329`, `2001:DB8::FF00:42:8329`)
r(`0000:0000:0000:0000:0000:0000:0000:0001`, `0000:0000:0000:0000:0000:0000:0000:0001`)
r(`::1`, `::1`)
r(`255.255.255.256`)
r(`:1`)
reset()
tdir := t.TempDir()
simple := filepath.Join(tdir, "simple.py")
cli_args = []string{"--customize-processing", simple, "extra1"}
os.WriteFile(simple, []byte(`
def mark(text, args, Mark, extra_cli_args, *a):
import re
for idx, m in enumerate(re.finditer(r'\w+', text)):
start, end = m.span()
mark_text = text[start:end].replace('\n', '').replace('\0', '')
yield Mark(idx, start, end, mark_text, {"idx": idx, "args": extra_cli_args})
`), 0o600)
opts.Type = "regex"
opts.CustomizeProcessing = simple
marks := r("漢字 b", `漢字`, `b`)
if diff := cmp.Diff(marks[0].Groupdict, map[string]any{"idx": float64(0), "args": []any{"extra1"}}); diff != "" {
t.Fatalf("Did not get expected groupdict from custom processor:\n%s", diff)
}
opts.Regex = "b"
os.WriteFile(simple, []byte(""), 0o600)
r("a b", `b`)
}

View File

@@ -0,0 +1,5 @@
// generated by gen-wcwidth.py, do not edit
package hints
const URL_DELIMITERS = `\x00-\x09\x0b-\x0c\x0e-\x20\x7f-\xa0\xad\x{600}-\x{605}\x{61c}\x{6dd}\x{70f}\x{890}-\x{891}\x{8e2}\x{1680}\x{180e}\x{2000}-\x{200f}\x{2028}-\x{202f}\x{205f}-\x{2064}\x{2066}-\x{206f}\x{3000}\x{d800}-\x{f8ff}\x{feff}\x{fff9}-\x{fffb}\x{110bd}\x{110cd}\x{13430}-\x{1343f}\x{1bca0}-\x{1bca3}\x{1d173}-\x{1d17a}\x{e0001}\x{e0020}-\x{e007f}\x{f0000}-\x{ffffd}\x{100000}-\x{10fffd}`