Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8825cd3811 | ||
|
|
3d9a5d9970 | ||
|
|
1f9e195fa6 | ||
|
|
73c012c76c | ||
|
|
2ace57404b | ||
|
|
8c4b5e088b | ||
|
|
69920a73d7 | ||
|
|
ae76a3467a | ||
|
|
701107cda4 | ||
|
|
b99565959b | ||
|
|
67aa7ce04d |
@@ -297,6 +297,13 @@
|
||||
},
|
||||
"description": "MCP (Model Context Protocol) server configurations"
|
||||
},
|
||||
"instructions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Additional instruction files or patterns to include"
|
||||
},
|
||||
"experimental": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -100,7 +100,7 @@ export const TuiCommand = cmd({
|
||||
UI.empty()
|
||||
UI.println(UI.logo(" "))
|
||||
const result = await Bun.spawn({
|
||||
cmd: [process.execPath, "auth", "login"],
|
||||
cmd: [...getOpencodeCommand(), "auth", "login"],
|
||||
cwd: process.cwd(),
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
@@ -112,3 +112,25 @@ export const TuiCommand = cmd({
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Get the correct command to run opencode CLI
|
||||
* In development: ["bun", "run", "packages/opencode/src/index.ts"]
|
||||
* In production: ["/path/to/opencode"]
|
||||
*/
|
||||
function getOpencodeCommand(): string[] {
|
||||
// Check if OPENCODE_BIN_PATH is set (used by shell wrapper scripts)
|
||||
if (process.env["OPENCODE_BIN_PATH"]) {
|
||||
return [process.env["OPENCODE_BIN_PATH"]]
|
||||
}
|
||||
|
||||
const execPath = process.execPath.toLowerCase()
|
||||
|
||||
if (Installation.isDev()) {
|
||||
// In development, use bun to run the TypeScript entry point
|
||||
return [execPath, "run", process.argv[1]]
|
||||
}
|
||||
|
||||
// In production, use the current executable path
|
||||
return [process.execPath]
|
||||
}
|
||||
|
||||
@@ -176,6 +176,10 @@ export namespace Config {
|
||||
.record(z.string(), Mcp)
|
||||
.optional()
|
||||
.describe("MCP (Model Context Protocol) server configurations"),
|
||||
instructions: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Additional instruction files or patterns to include"),
|
||||
experimental: z
|
||||
.object({
|
||||
hook: z
|
||||
|
||||
@@ -57,6 +57,7 @@ export namespace LSPServer {
|
||||
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||
})
|
||||
if (!bin) {
|
||||
if (!Bun.which("go")) return
|
||||
log.info("installing gopls")
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
|
||||
|
||||
@@ -134,7 +134,7 @@ The user will primarily request you perform software engineering tasks. This inc
|
||||
- Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially.
|
||||
- Implement the solution using all tools available to you
|
||||
- Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach.
|
||||
- VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) with Bash if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to CLAUDE.md so that you will know to run it next time.
|
||||
- VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) with Bash if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to AGENTS.md so that you will know to run it next time.
|
||||
NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive.
|
||||
|
||||
- Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are NOT part of the user's provided input or the tool result.
|
||||
|
||||
@@ -2,6 +2,7 @@ import { App } from "../app/app"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { Global } from "../global"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Config } from "../config/config"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
|
||||
@@ -55,8 +56,10 @@ export namespace SystemPrompt {
|
||||
"CLAUDE.md",
|
||||
"CONTEXT.md", // deprecated
|
||||
]
|
||||
|
||||
export async function custom() {
|
||||
const { cwd, root } = App.info().path
|
||||
const config = await Config.get()
|
||||
const found = []
|
||||
for (const item of CUSTOM_FILES) {
|
||||
const matches = await Filesystem.findUp(item, cwd, root)
|
||||
@@ -72,6 +75,18 @@ export namespace SystemPrompt {
|
||||
.text()
|
||||
.catch(() => ""),
|
||||
)
|
||||
|
||||
if (config.instructions) {
|
||||
for (const instruction of config.instructions) {
|
||||
try {
|
||||
const matches = await Filesystem.globUp(instruction, cwd, root)
|
||||
found.push(...matches.map((x) => Bun.file(x).text()))
|
||||
} catch {
|
||||
continue // Skip invalid glob patterns
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(found).then((result) => result.filter(Boolean))
|
||||
}
|
||||
|
||||
|
||||
@@ -67,9 +67,12 @@ export namespace Share {
|
||||
}
|
||||
|
||||
export async function remove(id: string) {
|
||||
const share = await Session.getShare(id).catch(() => {})
|
||||
if (!share) return
|
||||
const { secret } = share
|
||||
return fetch(`${URL}/share_delete`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id }),
|
||||
body: JSON.stringify({ id, secret }),
|
||||
}).then((x) => x.json())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,28 @@ export namespace Filesystem {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export async function globUp(pattern: string, start: string, stop?: string) {
|
||||
let current = start
|
||||
const result = []
|
||||
while (true) {
|
||||
try {
|
||||
const glob = new Bun.Glob(pattern)
|
||||
for await (const match of glob.scan({
|
||||
cwd: current,
|
||||
onlyFiles: true,
|
||||
dot: true,
|
||||
})) {
|
||||
result.push(join(current, match))
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid glob patterns
|
||||
}
|
||||
if (stop === current) break
|
||||
const parent = dirname(current)
|
||||
if (parent === current) break
|
||||
current = parent
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ const (
|
||||
SessionNewCommand CommandName = "session_new"
|
||||
SessionListCommand CommandName = "session_list"
|
||||
SessionShareCommand CommandName = "session_share"
|
||||
SessionUnshareCommand CommandName = "session_unshare"
|
||||
SessionInterruptCommand CommandName = "session_interrupt"
|
||||
SessionCompactCommand CommandName = "session_compact"
|
||||
ToolDetailsCommand CommandName = "tool_details"
|
||||
@@ -160,6 +161,12 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
||||
Keybindings: parseBindings("<leader>s"),
|
||||
Trigger: "share",
|
||||
},
|
||||
{
|
||||
Name: SessionUnshareCommand,
|
||||
Description: "unshare session",
|
||||
Keybindings: parseBindings("<leader>u"),
|
||||
Trigger: "unshare",
|
||||
},
|
||||
{
|
||||
Name: SessionInterruptCommand,
|
||||
Description: "interrupt session",
|
||||
@@ -289,7 +296,7 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
||||
{
|
||||
Name: MessagesRevertCommand,
|
||||
Description: "revert message",
|
||||
Keybindings: parseBindings("<leader>u"),
|
||||
Keybindings: parseBindings("<leader>r"),
|
||||
},
|
||||
{
|
||||
Name: AppExitCommand,
|
||||
|
||||
@@ -162,18 +162,16 @@ func renderContentBlock(
|
||||
|
||||
if highlight {
|
||||
style = style.
|
||||
BorderLeftBackground(t.Primary()).
|
||||
BorderLeftForeground(t.Primary()).
|
||||
BorderRightForeground(t.Primary()).
|
||||
BorderRightBackground(t.Primary())
|
||||
BorderLeftForeground(borderColor).
|
||||
BorderRightForeground(borderColor)
|
||||
}
|
||||
}
|
||||
|
||||
if highlight {
|
||||
style = style.
|
||||
Foreground(t.Text()).
|
||||
Bold(true).
|
||||
Background(t.BackgroundElement())
|
||||
Background(t.BackgroundElement()).
|
||||
Bold(true)
|
||||
}
|
||||
|
||||
content = style.Render(content)
|
||||
@@ -211,7 +209,7 @@ func renderContentBlock(
|
||||
)
|
||||
header = styles.NewStyle().Background(t.Background()).Padding(0, 1).Render(header)
|
||||
|
||||
content = "\n\n\n" + header + "\n\n" + content + "\n\n"
|
||||
content = "\n\n\n" + header + "\n\n" + content + "\n\n\n"
|
||||
}
|
||||
|
||||
return content
|
||||
@@ -229,7 +227,9 @@ func renderText(
|
||||
) string {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
timestamp := time.UnixMilli(int64(message.Metadata.Time.Created)).Local().Format("02 Jan 2006 03:04 PM")
|
||||
timestamp := time.UnixMilli(int64(message.Metadata.Time.Created)).
|
||||
Local().
|
||||
Format("02 Jan 2006 03:04 PM")
|
||||
if time.Now().Format("02 Jan 2006") == timestamp[:11] {
|
||||
// don't show the date if it's today
|
||||
timestamp = timestamp[12:]
|
||||
@@ -338,8 +338,10 @@ func renderToolDetails(
|
||||
finished := result != nil && *result != ""
|
||||
t := theme.CurrentTheme()
|
||||
backgroundColor := t.BackgroundPanel()
|
||||
borderColor := t.BackgroundPanel()
|
||||
if highlight {
|
||||
backgroundColor = t.BackgroundElement()
|
||||
borderColor = t.BorderActive()
|
||||
}
|
||||
|
||||
switch toolCall.ToolInvocation.ToolName {
|
||||
@@ -362,7 +364,11 @@ func renderToolDetails(
|
||||
diff.WithWidth(width-2),
|
||||
)
|
||||
body = strings.TrimSpace(formattedDiff)
|
||||
style := styles.NewStyle().Background(backgroundColor).Foreground(t.TextMuted()).Padding(1, 2).Width(width - 4)
|
||||
style := styles.NewStyle().
|
||||
Background(backgroundColor).
|
||||
Foreground(t.TextMuted()).
|
||||
Padding(1, 2).
|
||||
Width(width - 4)
|
||||
if highlight {
|
||||
style = style.Foreground(t.Text()).Bold(true)
|
||||
}
|
||||
@@ -375,7 +381,14 @@ func renderToolDetails(
|
||||
title := renderToolTitle(toolCall, messageMetadata, width)
|
||||
title = style.Render(title)
|
||||
content := title + "\n" + body
|
||||
content = renderContentBlock(app, content, highlight, width, WithPadding(0))
|
||||
content = renderContentBlock(
|
||||
app,
|
||||
content,
|
||||
highlight,
|
||||
width,
|
||||
WithPadding(0),
|
||||
WithBorderColor(borderColor),
|
||||
)
|
||||
return content
|
||||
}
|
||||
}
|
||||
@@ -478,7 +491,7 @@ func renderToolDetails(
|
||||
|
||||
title := renderToolTitle(toolCall, messageMetadata, width)
|
||||
content := title + "\n\n" + body
|
||||
return renderContentBlock(app, content, highlight, width)
|
||||
return renderContentBlock(app, content, highlight, width, WithBorderColor(borderColor))
|
||||
}
|
||||
|
||||
func renderToolName(name string) string {
|
||||
@@ -489,8 +502,8 @@ func renderToolName(name string) string {
|
||||
return "Plan"
|
||||
default:
|
||||
normalizedName := name
|
||||
if strings.HasPrefix(name, "opencode_") {
|
||||
normalizedName = strings.TrimPrefix(name, "opencode_")
|
||||
if after, ok := strings.CutPrefix(name, "opencode_"); ok {
|
||||
normalizedName = after
|
||||
}
|
||||
return cases.Title(language.Und).String(normalizedName)
|
||||
}
|
||||
@@ -651,7 +664,10 @@ func renderDiagnostics(metadata opencode.MessageMetadataTool, filePath string) s
|
||||
}
|
||||
line := diag.Range.Start.Line + 1 // 1-based
|
||||
column := diag.Range.Start.Character + 1 // 1-based
|
||||
errorDiagnostics = append(errorDiagnostics, fmt.Sprintf("Error [%d:%d] %s", line, column, diag.Message))
|
||||
errorDiagnostics = append(
|
||||
errorDiagnostics,
|
||||
fmt.Sprintf("Error [%d:%d] %s", line, column, diag.Message),
|
||||
)
|
||||
}
|
||||
if len(errorDiagnostics) == 0 {
|
||||
return ""
|
||||
|
||||
@@ -309,9 +309,12 @@ func (m *messagesComponent) header(width int) string {
|
||||
base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
|
||||
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
|
||||
headerLines := []string{}
|
||||
headerLines = append(headerLines, util.ToMarkdown("# "+m.app.Session.Title, width-6, t.Background()))
|
||||
headerLines = append(
|
||||
headerLines,
|
||||
util.ToMarkdown("# "+m.app.Session.Title, width-6, t.Background()),
|
||||
)
|
||||
if m.app.Session.Share.URL != "" {
|
||||
headerLines = append(headerLines, muted(m.app.Session.Share.URL))
|
||||
headerLines = append(headerLines, muted(m.app.Session.Share.URL+" /unshare"))
|
||||
} else {
|
||||
headerLines = append(headerLines, base("/share")+muted(" to create a shareable link"))
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/formatters"
|
||||
@@ -575,7 +576,10 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
|
||||
ansiSequences[visibleIdx] = lastAnsiSeq
|
||||
}
|
||||
visibleIdx++
|
||||
i++
|
||||
|
||||
// Properly advance by UTF-8 rune, not byte
|
||||
_, size := utf8.DecodeRuneInString(content[i:])
|
||||
i += size
|
||||
}
|
||||
|
||||
// Apply highlighting
|
||||
@@ -622,8 +626,9 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
|
||||
}
|
||||
}
|
||||
|
||||
// Get current character
|
||||
char := string(content[i])
|
||||
// Get current character (properly handle UTF-8)
|
||||
r, size := utf8.DecodeRuneInString(content[i:])
|
||||
char := string(r)
|
||||
|
||||
if inSelection {
|
||||
// Get the current styling
|
||||
@@ -657,7 +662,7 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
|
||||
}
|
||||
|
||||
currentPos++
|
||||
i++
|
||||
i += size
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
|
||||
@@ -37,7 +37,11 @@ func (m statusComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (m statusComponent) logo() string {
|
||||
t := theme.CurrentTheme()
|
||||
base := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundElement()).Render
|
||||
emphasis := styles.NewStyle().Foreground(t.Text()).Background(t.BackgroundElement()).Bold(true).Render
|
||||
emphasis := styles.NewStyle().
|
||||
Foreground(t.Text()).
|
||||
Background(t.BackgroundElement()).
|
||||
Bold(true).
|
||||
Render
|
||||
|
||||
open := base("open")
|
||||
code := emphasis("code ")
|
||||
@@ -72,19 +76,16 @@ func formatTokensAndCost(tokens float64, contextWindow float64, cost float64) st
|
||||
formattedCost := fmt.Sprintf("$%.2f", cost)
|
||||
percentage := (float64(tokens) / float64(contextWindow)) * 100
|
||||
|
||||
return fmt.Sprintf("Context: %s (%d%%), Cost: %s", formattedTokens, int(percentage), formattedCost)
|
||||
return fmt.Sprintf(
|
||||
"Context: %s (%d%%), Cost: %s",
|
||||
formattedTokens,
|
||||
int(percentage),
|
||||
formattedCost,
|
||||
)
|
||||
}
|
||||
|
||||
func (m statusComponent) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
if m.app.Session.ID == "" {
|
||||
return styles.NewStyle().
|
||||
Background(t.Background()).
|
||||
Width(m.width).
|
||||
Height(2).
|
||||
Render("")
|
||||
}
|
||||
|
||||
logo := m.logo()
|
||||
|
||||
cwd := styles.NewStyle().
|
||||
|
||||
@@ -41,7 +41,7 @@ const (
|
||||
)
|
||||
|
||||
const interruptDebounceTimeout = 1 * time.Second
|
||||
const fileViewerFullWidthCutoff = 200
|
||||
const fileViewerFullWidthCutoff = 160
|
||||
|
||||
type appModel struct {
|
||||
width, height int
|
||||
@@ -107,6 +107,26 @@ var BUGGED_SCROLL_KEYS = map[string]bool{
|
||||
";": true,
|
||||
}
|
||||
|
||||
func isScrollRelatedInput(keyString string) bool {
|
||||
if len(keyString) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, char := range keyString {
|
||||
charStr := string(char)
|
||||
if !BUGGED_SCROLL_KEYS[charStr] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(keyString) > 3 &&
|
||||
(keyString[len(keyString)-1] == 'M' || keyString[len(keyString)-1] == 'm') {
|
||||
return true
|
||||
}
|
||||
|
||||
return len(keyString) > 1
|
||||
}
|
||||
|
||||
func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
var cmds []tea.Cmd
|
||||
@@ -114,7 +134,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyPressMsg:
|
||||
keyString := msg.String()
|
||||
if time.Since(a.lastScroll) < time.Millisecond*100 && BUGGED_SCROLL_KEYS[keyString] {
|
||||
if time.Since(a.lastScroll) < time.Millisecond*100 && (BUGGED_SCROLL_KEYS[keyString] || isScrollRelatedInput(keyString)) {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
@@ -807,6 +827,17 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
||||
shareUrl := response.Share.URL
|
||||
cmds = append(cmds, tea.SetClipboard(shareUrl))
|
||||
cmds = append(cmds, toast.NewSuccessToast("Share URL copied to clipboard!"))
|
||||
case commands.SessionUnshareCommand:
|
||||
if a.app.Session.ID == "" {
|
||||
return a, nil
|
||||
}
|
||||
_, err := a.app.Client.Session.Unshare(context.Background(), a.app.Session.ID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to unshare session", "error", err)
|
||||
return a, toast.NewErrorToast("Failed to unshare session")
|
||||
}
|
||||
a.app.Session.Share.URL = ""
|
||||
cmds = append(cmds, toast.NewSuccessToast("Session unshared successfully"))
|
||||
case commands.SessionInterruptCommand:
|
||||
if a.app.Session.ID == "" {
|
||||
return a, nil
|
||||
|
||||
@@ -2,49 +2,39 @@ package util
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// MapReducePar performs a parallel map-reduce operation on a slice of items.
|
||||
// It applies a function to each item in the slice concurrently,
|
||||
// and combines the results serially using a reducer returned from
|
||||
// each one of the functions, allowing the use of closures.
|
||||
func MapReducePar[a, b any](items []a, init b, fn func(a) func(b) b) b {
|
||||
itemCount := len(items)
|
||||
locks := make([]*sync.Mutex, itemCount)
|
||||
mapped := make([]func(b) b, itemCount)
|
||||
func mapParallel[in, out any](items []in, fn func(in) out) chan out {
|
||||
mapChans := make([]chan out, 0, len(items))
|
||||
|
||||
for i, value := range items {
|
||||
lock := &sync.Mutex{}
|
||||
lock.Lock()
|
||||
locks[i] = lock
|
||||
for _, v := range items {
|
||||
ch := make(chan out)
|
||||
mapChans = append(mapChans, ch)
|
||||
go func() {
|
||||
defer lock.Unlock()
|
||||
mapped[i] = fn(value)
|
||||
defer close(ch)
|
||||
ch <- fn(v)
|
||||
}()
|
||||
}
|
||||
|
||||
result := init
|
||||
for i := range itemCount {
|
||||
locks[i].Lock()
|
||||
defer locks[i].Unlock()
|
||||
f := mapped[i]
|
||||
if f != nil {
|
||||
result = f(result)
|
||||
}
|
||||
}
|
||||
resultChan := make(chan out)
|
||||
|
||||
return result
|
||||
go func() {
|
||||
defer close(resultChan)
|
||||
for _, ch := range mapChans {
|
||||
v := <-ch
|
||||
resultChan <- v
|
||||
}
|
||||
}()
|
||||
|
||||
return resultChan
|
||||
}
|
||||
|
||||
// WriteStringsPar allows to iterate over a list and compute strings in parallel,
|
||||
// yet write them in order.
|
||||
func WriteStringsPar[a any](sb *strings.Builder, items []a, fn func(a) string) {
|
||||
MapReducePar(items, sb, func(item a) func(*strings.Builder) *strings.Builder {
|
||||
str := fn(item)
|
||||
return func(sbdr *strings.Builder) *strings.Builder {
|
||||
sbdr.WriteString(str)
|
||||
return sbdr
|
||||
}
|
||||
})
|
||||
ch := mapParallel(items, fn)
|
||||
|
||||
for v := range ch {
|
||||
sb.WriteString(v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package util_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sst/opencode/internal/util"
|
||||
)
|
||||
|
||||
func TestWriteStringsPar(t *testing.T) {
|
||||
items := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
sb := strings.Builder{}
|
||||
util.WriteStringsPar(&sb, items, func(i int) string {
|
||||
// sleep for the inverse duration so that later items finish first
|
||||
time.Sleep(time.Duration(10-i) * time.Millisecond)
|
||||
return strconv.Itoa(i)
|
||||
})
|
||||
if sb.String() != "0123456789" {
|
||||
t.Fatalf("expected 0123456789, got %s", sb.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user