Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions Glint/Agent/ShellRcBlock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,36 @@ enum ShellRcBlock {
/// Insert or replace a fenced block delimited by `begin` and `end`. The
/// returned text contains exactly one such block.
///
/// - Empty input → just `block + "\n"`.
/// - Empty input → just `block` (ending in one newline).
/// - Existing block (located by line-anchored sentinel match) → replaced
/// in place; surrounding content untouched.
/// - No existing block → appended with one blank line above so we don't
/// smash into a user's last line.
///
/// `block` is normalized to end in exactly one newline. The callers'
/// blocks are Swift multiline literals, which end at the closing
/// delimiter WITHOUT one, and pasting such a block over a located range
/// used to glue whatever followed the block onto the end sentinel
/// (#124) — silently commenting it out, and re-gluing it on every
/// launch. Normalizing here also heals an already-glued file the next
/// time the block is rewritten.
static func upsert(in text: String,
begin: String,
end: String,
block: String) -> String {
var block = block
while block.hasSuffix("\n") { block.removeLast() }
block += "\n"
if let range = locate(in: text, begin: begin, end: end) {
var out = text
out.replaceSubrange(range, with: block)
return out
}
if text.isEmpty { return block + "\n" }
if text.isEmpty { return block }
var out = text
if !out.hasSuffix("\n") { out += "\n" } // newline before block
if !out.hasSuffix("\n\n") { out += "\n" } // one blank line above block
out += block + "\n"
out += block
return out
}

Expand Down
14 changes: 8 additions & 6 deletions Glint/Pane/InlineSuggestionInstaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,17 +147,19 @@ enum InlineSuggestionInstaller {
let content = """
# Glint inline suggestions — sourced via the fenced block in ~/.zshrc.
# Skip if the user already has zsh-autosuggestions loaded by their own
# config (oh-my-zsh / prezto users): we respect their setup.
if [[ -z ${ZSH_AUTOSUGGEST_VERSION-} ]]; then
# config (oh-my-zsh / prezto users): we respect their setup. Probe the
# _zsh_autosuggest_start hook the plugin defines when sourced — not
# ZSH_AUTOSUGGEST_VERSION, which zsh-autosuggestions never sets, so
# the old guard here could never fire.
if (( ! $+functions[_zsh_autosuggest_start] )); then
ZSH_AUTOSUGGEST_STRATEGY=(history)
: ${ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE:='fg=8'}
: ${ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE:=200}
# No manual bindkey needed: the plugin's default
# ZSH_AUTOSUGGEST_ACCEPT_WIDGETS already binds → (forward-char)
# and End (end-of-line) to accept the suggestion.
if [[ -r "$HOME/.config/glint/zsh-autosuggestions.zsh" ]]; then
source "$HOME/.config/glint/zsh-autosuggestions.zsh"
# → and End accept the full suggestion. Tab stays as zsh's
# native completion (don't repurpose it).
bindkey '^[[C' autosuggest-accept 2>/dev/null
bindkey '^E' autosuggest-accept 2>/dev/null
fi
fi
"""
Expand Down
36 changes: 34 additions & 2 deletions GlintTests/ShellRcBlockTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,46 @@ final class ShellRcBlockTests: XCTestCase {
}

func testUpsertReplacesBlockInPlace() {
// `upsert` consumes the newline after `end`, so the replacement block
// must carry its own trailing newline (the real managed block does).
// Replacement must leave surrounding lines intact; `upsert` normalizes
// the block's trailing newline itself (see the #124 tests below).
let old = "pre\n\(begin)\nold body\n\(end)\npost\n"
let new = "\(begin)\nnew body\n\(end)\n"
let result = ShellRcBlock.upsert(in: old, begin: begin, end: end, block: new)
XCTAssertEqual(result, "pre\n\(begin)\nnew body\n\(end)\npost\n")
}

/// #124: the real managed blocks are Swift multiline literals, which end
/// at the closing delimiter WITHOUT a trailing newline. Replacing a block
/// with such a literal used to glue the line after the block onto the end
/// sentinel, silently commenting it out — and re-gluing it on every
/// launch, so a manual fix didn't survive an app restart.
func testUpsertReplaceCarriesTrailingNewline() {
let old = "pre\n\(begin)\nold body\n\(end)\npost\n"
let new = "\(begin)\nnew body\n\(end)" // no trailing newline
let result = ShellRcBlock.upsert(in: old, begin: begin, end: end, block: new)
XCTAssertEqual(result, "pre\n\(begin)\nnew body\n\(end)\npost\n")
}

/// A file already damaged by the #124 glue heals on the next launch: the
/// line stuck to the end sentinel lands back on its own line. (The real
/// damaged file ends with a newline — the glued line swallowed the one
/// after the sentinel, not the file's own.)
func testUpsertHealsLineGluedAfterEndSentinel() {
let glued = "pre\n\(begin)\nold body\n\(end)source /opt/homebrew/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh\n"
let new = "\(begin)\nnew body\n\(end)"
let result = ShellRcBlock.upsert(in: glued, begin: begin, end: end, block: new)
XCTAssertEqual(result, "pre\n\(begin)\nnew body\n\(end)\nsource /opt/homebrew/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh\n")
}

/// Launch-to-launch idempotency: re-upserting the same block must not
/// change the file (no write, no trailing-blank-line creep).
func testUpsertIsIdempotent() {
let block = "\(begin)\nx\n\(end)"
let first = ShellRcBlock.upsert(in: "user config\n", begin: begin, end: end, block: block)
let second = ShellRcBlock.upsert(in: first, begin: begin, end: end, block: block)
XCTAssertEqual(second, first)
}

func testUpsertAppendsWhenAbsent() {
let result = ShellRcBlock.upsert(in: "user config\n",
begin: begin, end: end,
Expand Down
Loading