feat(bookmark): prefill bookmark name via open_set_bookmark value arg (#652)

* feat(bookmark): allow prefilling name via open_set_bookmark value arg

Adds an optional `value` arg to the `revisions.open_set_bookmark` action
so Lua scripts can prefill the bookmark input dynamically (e.g. with a
date-stamped prefix). Introduces `$string?(name)` annotation syntax for
optional string args mirroring the existing `$bool()` behavior.

* chore: go fmt

---------

Co-authored-by: ibrahim dursun <ibrahim@dursun.cc>
This commit is contained in:
Robert Deaton
2026-04-27 20:48:35 +01:00
committed by GitHub
co-authored by ibrahim dursun
parent a9bd116a1f
commit 3a29862bce
9 changed files with 50 additions and 14 deletions
+18 -3
View File
@@ -809,10 +809,10 @@ func validateSetValueType(fieldType, value string, enums map[string][]enumValueM
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
return nil
}
if isStringArg(value) {
if isStringArg(value) || isOptionalStringArg(value) {
return nil
}
return fmt.Errorf("expected quoted string or $string(...), got %q", value)
return fmt.Errorf("expected quoted string, $string(...), or $string?(...), got %q", value)
default:
if argName, ok := parseEnumArg(value); ok {
if len(enums[fieldType]) == 0 {
@@ -852,6 +852,10 @@ func isStringArg(value string) bool {
return strings.HasPrefix(value, "$string(") && strings.HasSuffix(value, ")")
}
func isOptionalStringArg(value string) bool {
return strings.HasPrefix(value, "$string?(") && strings.HasSuffix(value, ")")
}
func parseArgRef(fieldType, value string, enums map[string][]enumValueMeta) (name string, typ string, required bool, ok bool) {
if isBoolArg(value) {
name := strings.TrimSuffix(strings.TrimPrefix(value, "$bool("), ")")
@@ -866,6 +870,13 @@ func parseArgRef(fieldType, value string, enums map[string][]enumValueMeta) (nam
}
return argName, enumSchemaForType(fieldType, enums), true, true
}
if isOptionalStringArg(value) {
name := strings.TrimSuffix(strings.TrimPrefix(value, "$string?("), ")")
if name == "" {
return "", "", false, false
}
return name, "string", false, true
}
if isStringArg(value) {
name := strings.TrimSuffix(strings.TrimPrefix(value, "$string("), ")")
if name == "" {
@@ -980,6 +991,10 @@ func renderValue(fieldType, value string, enums map[string][]enumValueMeta) stri
name := strings.TrimSuffix(strings.TrimPrefix(value, "$string("), ")")
return fmt.Sprintf("actionargs.StringArg(args, %q, \"\")", name)
}
if isOptionalStringArg(value) {
name := strings.TrimSuffix(strings.TrimPrefix(value, "$string?("), ")")
return fmt.Sprintf("actionargs.StringArg(args, %q, \"\")", name)
}
if argName, ok := parseEnumArg(value); ok && len(enums[fieldType]) > 0 {
return fmt.Sprintf("enumArg%s(args, %q)", toCamel(fieldType), argName)
}
@@ -1004,7 +1019,7 @@ func rulesUseActionArgs(rules []bindRule) bool {
if strings.HasPrefix(value, "$bool(") && strings.HasSuffix(value, ")") {
return true
}
if isStringArg(value) {
if isStringArg(value) || isOptionalStringArg(value) {
return true
}
}
+1 -1
View File
@@ -231,7 +231,7 @@ function wait_refresh() end
---@field open_inline_describe fun()
---@field open_rebase fun()
---@field open_revert fun()
---@field open_set_bookmark fun()
---@field open_set_bookmark fun(args: {value?: string})
---@field open_set_parents fun()
---@field open_squash fun()
---@field page_down fun()
+3
View File
@@ -303,6 +303,9 @@ var builtInActionArgSchemas = map[string]map[string]string{
"revisions.inline_describe.accept": {
"force": "bool",
},
"revisions.open_set_bookmark": {
"value": "string",
},
"revisions.rebase.apply": {
"force": "bool",
},
+1 -1
View File
@@ -307,7 +307,7 @@ func ResolveIntent(scope string, action keybindings.Action, args map[string]any)
case keybindings.Action("revisions.open_revert"):
return intents.OpenRevert{}, true
case keybindings.Action("revisions.open_set_bookmark"):
return intents.OpenSetBookmark{}, true
return intents.OpenSetBookmark{Value: actionargs.StringArg(args, "value", "")}, true
case keybindings.Action("revisions.open_set_parents"):
return intents.OpenSetParents{}, true
case keybindings.Action("revisions.open_squash"):
+4 -2
View File
@@ -77,8 +77,10 @@ type OpenGit struct{}
func (OpenGit) isIntent() {}
//jjui:bind scope=revisions action=open_set_bookmark
type OpenSetBookmark struct{}
//jjui:bind scope=revisions action=open_set_bookmark set=Value:$string?(value)
type OpenSetBookmark struct {
Value string
}
func (OpenSetBookmark) isIntent() {}
@@ -107,13 +107,14 @@ func (s *SetBookmarkOperation) Name() string {
return "set bookmark"
}
func NewSetBookmarkOperation(context *context.MainContext, changeId string) *SetBookmarkOperation {
func NewSetBookmarkOperation(context *context.MainContext, changeId string, initialValue string) *SetBookmarkOperation {
t := textinput.New()
t.ShowSuggestions = true
t.CharLimit = 120
t.Prompt = ""
t.SetValue("")
t.SetValue(initialValue)
t.CursorEnd()
t.Focus()
op := &SetBookmarkOperation{
@@ -15,8 +15,23 @@ func TestSetBookmarkModel_Update(t *testing.T) {
commandRunner.Expect(jj.BookmarkSet("revision", "name"))
defer commandRunner.Verify()
op := NewSetBookmarkOperation(test.NewTestContext(commandRunner), "revision")
op := NewSetBookmarkOperation(test.NewTestContext(commandRunner), "revision", "")
test.SimulateModel(op, op.Init())
test.SimulateModel(op, test.Type("name"))
test.SimulateModel(op, func() tea.Msg { return intents.Apply{} })
}
func TestSetBookmarkModel_Prefill(t *testing.T) {
commandRunner := test.NewTestCommandRunner(t)
commandRunner.Expect(jj.BookmarkListMovable("revision"))
commandRunner.Expect(jj.BookmarkSet("revision", "rdeaton/20260425/feature"))
defer commandRunner.Verify()
op := NewSetBookmarkOperation(test.NewTestContext(commandRunner), "revision", "rdeaton/20260425/")
test.SimulateModel(op, op.Init())
if got := op.name.Value(); got != "rdeaton/20260425/" {
t.Fatalf("expected prefilled value %q, got %q", "rdeaton/20260425/", got)
}
test.SimulateModel(op, test.Type("feature"))
test.SimulateModel(op, func() tea.Msg { return intents.Apply{} })
}
+3 -3
View File
@@ -643,7 +643,7 @@ func (m *Model) HandleIntent(intent intents.Intent) (tea.Cmd, bool) {
case intents.OpenSetParents:
return m.startSetParents(intent), true
case intents.OpenSetBookmark:
return m.startBookmarkSet(), true
return m.startBookmarkSet(intent), true
case intents.RevisionsToggleSelect:
commit := m.SelectedRevision()
if commit == nil {
@@ -681,12 +681,12 @@ func (m *Model) HandleIntent(intent intents.Intent) (tea.Cmd, bool) {
return nil, false
}
func (m *Model) startBookmarkSet() tea.Cmd {
func (m *Model) startBookmarkSet(intent intents.OpenSetBookmark) tea.Cmd {
rev := m.SelectedRevision()
if rev == nil {
return nil
}
return m.setBaseOperation(bookmark.NewSetBookmarkOperation(m.context, rev.GetChangeId()))
return m.setBaseOperation(bookmark.NewSetBookmarkOperation(m.context, rev.GetChangeId(), intent.Value))
}
func (m *Model) refresh(intent intents.Refresh) tea.Cmd {
+1 -1
View File
@@ -1327,7 +1327,7 @@ func Test_Update_SetBookmarkTypingDoesNotTogglePreview(t *testing.T) {
model := NewUI(ctx)
model.previewModel.SetVisible(true)
op := bookmark.NewSetBookmarkOperation(ctx, "abc123")
op := bookmark.NewSetBookmarkOperation(ctx, "abc123", "")
test.SimulateModel(op, op.Init())
model.Update(common.RestoreOperationMsg{Operation: op})
require.False(t, model.revisions.InNormalMode(), "set bookmark operation should be active")