镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

A profile created before InitializeAsync() loses the stored profile's name and description, and resolves duplicate stored chords to a different command than a fresh load (gaps in #206) #247

Open

Description

@matt-edmondson

What's wrong

#206 made InitializeAsync() merge a stored profile into a same-id profile already in memory (CreateDefaultProfile() called early, or a profile manager shared through DI), instead of dropping it. MergeStoredBindings (Keybinding/KeybindingManager.cs:160-170) still loses two things on that path:

  1. Name and description are dropped. Only bindings are merged. The in-memory placeholder's Name/Description ("Default" / "Default keybinding profile") stay, and the next SaveAsync writes them over the user's.
  2. Duplicate stored chords resolve differently. The merge walks stored.GetAllChords() in dictionary (file) order and keeps only the first binding per chord (usedChords.Add). A normal load keeps every duplicate and resolves them in ordinal command-id order (Profile.FindCommand, Keybinding/Models/Profile.cs:254), and SetStoredChord's docs say such legacy data should load unchanged. So the same file runs a different command depending on call order. The binding a fresh load would have run gets dropped, and the next save deletes it from disk.

Reproduction (HEAD 5d08140)

Name lost:

using (KeybindingManager m = new(dir)) {
    await m.InitializeAsync(); m.CreateDefaultProfile();
    m.Profiles.RenameProfile("default", "My Keys", "Tuned for vim");
    await m.SaveAsync();
}
using (KeybindingManager m = new(dir)) {
    m.CreateDefaultProfile(); await m.InitializeAsync();   // create before init
    Profile p = m.Profiles.GetProfile("default")!;
    Console.WriteLine($"{p.Name} / {p.Description}");
    await m.SaveAsync();
}

Output: Default / Default keybinding profile, which a third launch also reads back from disk. Expected: My Keys / Tuned for vim.

Duplicate chord order. Data directory:

  • commands.json: [{"id":"z.cmd","name":"Z"},{"id":"a.cmd","name":"A"}]
  • profiles.json: [{"id":"default","name":"Default","chords":{"z.cmd":{"notes":["CTRL","S"]},"a.cmd":{"notes":["CTRL","S"]}}}]
  • active-profile.json: {"activeProfileId":"default"}
// init first:          ExecuteChord(Ctrl+S) = a.cmd, ChordCount = 2
// CreateDefaultProfile() then init: ExecuteChord(Ctrl+S) = z.cmd, ChordCount = 1

Why it matters

Both cases lose data silently and permanently on a path #206 accepted as supported. A user's renamed profile reverts. With legacy duplicate data, a shortcut changes which command it runs and one binding disappears from disk.

Suggested fix

In MergeStoredBindings:

  • Take the stored Name/Description (existing.Rename(stored.Name, stored.Description)), at least when the in-memory profile is still the untouched placeholder.
  • Copy stored bindings so the result matches a fresh load. Walk them in StringComparer.Ordinal command-id order, as FindCommand does. Or, for a chord the in-memory profile doesn't use, copy every stored binding for it with SetStoredChord.

Acceptance criteria

  • Tests that call CreateDefaultProfile() before InitializeAsync() and check that (a) a stored renamed profile keeps its name and description through a save and reload, and (b) the duplicate-chord data above runs a.cmd and keeps both bindings, the same as init-first.

Activity

  1. matt-edmondson commented on Nov 12, 2026

    @matt-edmondson
    ContributorAuthor

    Triage


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions