fix(pi-natives-chunk): corrected chunk edits to reject invalid ^/~ ops
- Fixed chunk edits to reject invalid `^`/`~` operations on code-leaf chunks. - Fixed multiline replacement behavior to preserve literal insertion text and adjust multiline match starts. - Fixed unsupported-region handling to warn, fall back to whole-chunk edits, and clear invalid region targets. - Documented chunk-edit behavior for raw whitespace, parser boundaries, `sel="?"` refresh, markdown spacing, and streaming headers.
This commit is contained in:
@@ -75,6 +75,11 @@ struct ResolvedEditTarget {
|
||||
region: Option<ChunkRegion>,
|
||||
}
|
||||
|
||||
enum RegionFallback {
|
||||
Reject(String),
|
||||
Warn(String),
|
||||
}
|
||||
|
||||
const NORMALIZED_TAB_REPLACEMENT: &str = " ";
|
||||
const PRESERVED_TAB_REPLACEMENT: &str = "\t";
|
||||
|
||||
@@ -606,6 +611,7 @@ fn resolve_edit_target(
|
||||
warnings,
|
||||
)?
|
||||
.clone();
|
||||
let requested_region = region;
|
||||
let python_leaf_control_flow = state.language == "python"
|
||||
&& chunk.leaf
|
||||
&& matches!(
|
||||
@@ -618,17 +624,82 @@ fn resolve_edit_target(
|
||||
| ChunkKind::Elif
|
||||
| ChunkKind::Except
|
||||
);
|
||||
if chunk.prologue_end_byte.is_none()
|
||||
|| chunk.epilogue_start_byte.is_none()
|
||||
|| python_leaf_control_flow
|
||||
|| (chunk.kind == ChunkKind::Section && matches!(operation.op, ChunkEditOp::Put))
|
||||
{
|
||||
if let Some(action) = unsupported_region_action(state, &chunk, operation.op, requested_region) {
|
||||
match action {
|
||||
RegionFallback::Reject(reason) => return Err(reason),
|
||||
RegionFallback::Warn(reason) => warnings.push(reason),
|
||||
}
|
||||
region = None;
|
||||
} else if python_leaf_control_flow {
|
||||
region = None;
|
||||
}
|
||||
|
||||
Ok(ResolvedEditTarget { chunk, region })
|
||||
}
|
||||
|
||||
const fn region_label(region: ChunkRegion) -> &'static str {
|
||||
match region {
|
||||
ChunkRegion::Head => "^",
|
||||
ChunkRegion::Body => "~",
|
||||
}
|
||||
}
|
||||
|
||||
fn unsupported_region_action(
|
||||
state: &ChunkStateInner,
|
||||
chunk: &ChunkNode,
|
||||
op: ChunkEditOp,
|
||||
requested_region: Option<ChunkRegion>,
|
||||
) -> Option<RegionFallback> {
|
||||
let region = requested_region?;
|
||||
let label = region_label(region);
|
||||
let chunk_label = chunk_path_opt(chunk);
|
||||
let warn_on_region_fallback =
|
||||
matches!(
|
||||
state.language.as_str(),
|
||||
"markdown"
|
||||
| "md" | "handlebars"
|
||||
| "hbs" | "yaml"
|
||||
| "yml" | "json"
|
||||
| "jsonc"
|
||||
| "toml" | "text"
|
||||
| "txt"
|
||||
);
|
||||
|
||||
let detail = if state.language == "python"
|
||||
&& chunk.leaf
|
||||
&& matches!(
|
||||
chunk.kind,
|
||||
ChunkKind::If
|
||||
| ChunkKind::Loop
|
||||
| ChunkKind::Try
|
||||
| ChunkKind::Block
|
||||
| ChunkKind::Match
|
||||
| ChunkKind::Elif
|
||||
| ChunkKind::Except
|
||||
) {
|
||||
Some("Python compound-statement leaf chunks do not expose a safe body/head edit boundary")
|
||||
} else if chunk.prologue_end_byte.is_none() || chunk.epilogue_start_byte.is_none() {
|
||||
Some("this chunk has no body/head edit boundary")
|
||||
} else if chunk.kind == ChunkKind::Section && matches!(op, ChunkEditOp::Put) {
|
||||
Some("section writes do not expose a safe body/head edit boundary")
|
||||
} else {
|
||||
None
|
||||
}?;
|
||||
|
||||
if warn_on_region_fallback {
|
||||
return Some(RegionFallback::Warn(format!(
|
||||
"Region suffix `{label}` on {chunk_label} fell back to whole-chunk editing because \
|
||||
{detail}. Include the complete chunk content, including headings, fences, list markers, \
|
||||
or table rows."
|
||||
)));
|
||||
}
|
||||
|
||||
Some(RegionFallback::Reject(format!(
|
||||
"Region suffix `{label}` is not supported on {chunk_label}: {detail}. Use the unsuffixed \
|
||||
selector with complete replacement content, or edit a parent container's `~` body instead."
|
||||
)))
|
||||
}
|
||||
|
||||
fn find_current_batch_target_after_edit<'a>(
|
||||
state: &'a ChunkStateInner,
|
||||
before: &ChunkNode,
|
||||
@@ -690,6 +761,10 @@ fn update_current_batch_target(
|
||||
/// indentation. Detects the base indent of the first line in `original` and
|
||||
/// applies it to `replacement`.
|
||||
fn reindent_replacement(original: &str, replacement: &str) -> String {
|
||||
if replacement.contains('\n') {
|
||||
return replacement.to_string();
|
||||
}
|
||||
|
||||
let orig_indent = original
|
||||
.lines()
|
||||
.next()
|
||||
@@ -720,6 +795,36 @@ fn reindent_replacement(original: &str, replacement: &str) -> String {
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn expanded_match_start_for_multiline_replacement(
|
||||
source: &str,
|
||||
abs_start: usize,
|
||||
replacement: &str,
|
||||
) -> Option<usize> {
|
||||
if !replacement.contains('\n') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let line_start = source[..abs_start].rfind('\n').map_or(0, |idx| idx + 1);
|
||||
if line_start == abs_start {
|
||||
return None;
|
||||
}
|
||||
|
||||
let existing_prefix = &source[line_start..abs_start];
|
||||
if existing_prefix.is_empty()
|
||||
|| !existing_prefix
|
||||
.bytes()
|
||||
.all(|byte| matches!(byte, b' ' | b'\t'))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
if replacement.starts_with(existing_prefix) {
|
||||
Some(line_start)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to find `needle` in `haystack` by normalizing leading whitespace on each
|
||||
/// line. Returns `(byte_offset, byte_length)` of the match in `haystack`.
|
||||
fn find_indent_normalized(haystack: &str, needle: &str) -> Option<(usize, usize)> {
|
||||
@@ -822,8 +927,13 @@ fn apply_find_replace(
|
||||
};
|
||||
|
||||
let raw_replacement = operation.content.as_deref().unwrap_or_default();
|
||||
let abs_start = region_start + rel_offset;
|
||||
let mut abs_start = region_start + rel_offset;
|
||||
let abs_end = abs_start + match_len;
|
||||
if let Some(expanded_start) =
|
||||
expanded_match_start_for_multiline_replacement(&state.source, abs_start, raw_replacement)
|
||||
{
|
||||
abs_start = expanded_start;
|
||||
}
|
||||
|
||||
// Re-indent replacement to match the matched source's indentation when
|
||||
// indent normalization is active.
|
||||
@@ -2125,8 +2235,13 @@ fn normalize_insertion_boundary_content(
|
||||
} else {
|
||||
usize::from(prev_char.is_some() && prev_char != Some('\n'))
|
||||
};
|
||||
let leading_newlines_after = count_leading_newlines_after_offset(&state.source, offset);
|
||||
let suffix_newlines = if spacing.blank_line_after {
|
||||
2usize.saturating_sub(count_leading_newlines_after_offset(&state.source, offset))
|
||||
2usize.saturating_sub(leading_newlines_after)
|
||||
} else if leading_newlines_after == 1 && content.ends_with('\n') {
|
||||
// Preserve an existing blank-line separator when appending line-oriented
|
||||
// content immediately before the separator's single newline.
|
||||
1
|
||||
} else {
|
||||
usize::from(next_char.is_some() && next_char != Some('\n'))
|
||||
};
|
||||
@@ -3492,6 +3607,36 @@ function helper(): void {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_replace_preserves_multiline_replacement_indentation() {
|
||||
let source = "enum Status {\n\tRunning,\n\tStopped,\n}\n";
|
||||
let state = state_for(source, "rust");
|
||||
let chunk = state.inner().chunk("en_Sta").expect("en_Sta");
|
||||
|
||||
let result = apply_edits(&state, &EditParams {
|
||||
operations: vec![EditOperation {
|
||||
op: ChunkEditOp::Replace,
|
||||
sel: Some("en_Sta".to_owned()),
|
||||
crc: Some(chunk.checksum.clone()),
|
||||
region: None,
|
||||
content: Some("\tPaused,\n\tStopped,\n\tFailed,".to_owned()),
|
||||
find: Some("Stopped,".to_owned()),
|
||||
}],
|
||||
default_selector: None,
|
||||
default_crc: None,
|
||||
anchor_style: None,
|
||||
cwd: ".".to_owned(),
|
||||
file_path: "test.rs".to_owned(),
|
||||
normalize_indent: None,
|
||||
})
|
||||
.expect("edit should apply");
|
||||
|
||||
assert_eq!(
|
||||
result.diff_after,
|
||||
"enum Status {\n\tRunning,\n\tPaused,\n\tStopped,\n\tFailed,\n}\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_emits_only_changed_chain() {
|
||||
let source = "const a = 1;\n\nconst b = 2;\n\nconst c = 3;\n\nconst d = 4;\n\nconst e = 5;\n";
|
||||
@@ -4247,6 +4392,108 @@ function helper(): void {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_body_write_region_fallback_warns_before_whole_chunk_replace() {
|
||||
let source = "# Title\n\n## Alpha\n\nalpha body\n\n## Beta\n\nbeta body\n";
|
||||
let state = state_for(source, "markdown");
|
||||
let section = state
|
||||
.inner()
|
||||
.chunk("sct_Tit.sct_Alp")
|
||||
.expect("alpha section");
|
||||
|
||||
let result = apply_single_edit(&state, "test.md", EditOperation {
|
||||
op: ChunkEditOp::Put,
|
||||
sel: Some(format!("{}#{}~", section.path, section.checksum)),
|
||||
crc: None,
|
||||
region: None,
|
||||
content: Some("## Alpha\n\nnew body\n".to_owned()),
|
||||
find: None,
|
||||
});
|
||||
|
||||
assert!(
|
||||
result
|
||||
.diff_after
|
||||
.contains("## Alpha\n\nnew body\n\n## Beta"),
|
||||
"whole-section replacement should preserve the next heading separator: {:?}",
|
||||
result.diff_after
|
||||
);
|
||||
assert!(
|
||||
result
|
||||
.warnings
|
||||
.iter()
|
||||
.any(|warning| warning.contains("fell back to whole-chunk editing")),
|
||||
"markdown region fallback should warn: {:?}",
|
||||
result.warnings
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_table_body_append_keeps_row_continuity() {
|
||||
let source = "## Section\n\n| A |\n| --- |\n| one |\n\n## Next\n";
|
||||
let state = state_for(source, "markdown");
|
||||
let table = state
|
||||
.inner()
|
||||
.tree
|
||||
.chunks
|
||||
.iter()
|
||||
.find(|chunk| chunk.start_line == 3 && chunk.end_line == 5)
|
||||
.expect("table chunk");
|
||||
|
||||
let result = apply_single_edit(&state, "test.md", EditOperation {
|
||||
op: ChunkEditOp::Append,
|
||||
sel: Some(format!("{}#{}~", table.path, table.checksum)),
|
||||
crc: None,
|
||||
region: None,
|
||||
content: Some("| two |\n".to_owned()),
|
||||
find: None,
|
||||
});
|
||||
|
||||
assert!(
|
||||
result.diff_after.contains("| one |\n| two |\n\n## Next"),
|
||||
"appended table row should stay contiguous with the table and preserve next-section \
|
||||
spacing: {:?}",
|
||||
result.diff_after
|
||||
);
|
||||
assert!(
|
||||
result
|
||||
.warnings
|
||||
.iter()
|
||||
.any(|warning| warning.contains("fell back to whole-chunk editing")),
|
||||
"table body-region append fallback should warn: {:?}",
|
||||
result.warnings
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_fenced_python_body_write_preserves_code_indent() {
|
||||
let source = "```python\ndef outer():\n if cond:\n return 1\n```\n";
|
||||
let state = state_for(source, "markdown");
|
||||
let function = state
|
||||
.inner()
|
||||
.tree
|
||||
.chunks
|
||||
.iter()
|
||||
.find(|chunk| chunk.kind == ChunkKind::Function)
|
||||
.expect("embedded python function chunk");
|
||||
|
||||
let result = apply_single_edit(&state, "test.md", EditOperation {
|
||||
op: ChunkEditOp::Put,
|
||||
sel: Some(format!("{}#{}~", function.path, function.checksum)),
|
||||
crc: None,
|
||||
region: None,
|
||||
content: Some("if cond:\n\treturn 2\nreturn 3\n".to_owned()),
|
||||
find: None,
|
||||
});
|
||||
|
||||
assert!(
|
||||
result
|
||||
.diff_after
|
||||
.contains("def outer():\n if cond:\n return 2\n return 3\n```"),
|
||||
"embedded fenced Python body should keep 4-space code indentation: {:?}",
|
||||
result.diff_after
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rust_trait_members_are_addressable() {
|
||||
let source = "trait Handler {\n fn handle(&self, req: &str) -> String;\n fn \
|
||||
@@ -4296,7 +4543,7 @@ function helper(): void {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn body_region_on_leaf_without_delimiters_falls_back_to_full_chunk() {
|
||||
fn body_region_on_leaf_without_delimiters_is_rejected() {
|
||||
let source = "enum LogLevel {\n Debug,\n Info,\n Warn,\n Fatal,\n}\n";
|
||||
let state = state_for(source, "rust");
|
||||
let chunk = state
|
||||
@@ -4307,7 +4554,7 @@ function helper(): void {
|
||||
|
||||
for region_suffix in ["~", "^"] {
|
||||
let sel = format!("en_Log.vr_Inf#{}{}", chunk.checksum, region_suffix);
|
||||
let result = apply_edits(&state, &EditParams {
|
||||
let err = apply_edits(&state, &EditParams {
|
||||
operations: vec![EditOperation {
|
||||
op: ChunkEditOp::Put,
|
||||
sel: Some(sel),
|
||||
@@ -4323,12 +4570,16 @@ function helper(): void {
|
||||
file_path: "test.rs".to_owned(),
|
||||
normalize_indent: None,
|
||||
})
|
||||
.expect("leaf region should fall back to full chunk");
|
||||
.err()
|
||||
.expect("leaf region should be rejected");
|
||||
|
||||
assert!(
|
||||
result.diff_after.contains("Debug,\n Error,\n Warn,"),
|
||||
"{region_suffix} should replace the full leaf chunk, got: {}",
|
||||
result.diff_after
|
||||
err.contains("Region suffix"),
|
||||
"{region_suffix} should return a clear region error, got: {err}"
|
||||
);
|
||||
assert!(
|
||||
err.contains("unsuffixed selector"),
|
||||
"{region_suffix} error should mention the safe workaround, got: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5266,10 +5517,11 @@ function foo() {\n<<<<<<< HEAD\n\treturn bar();\n=======\n\treturn baz();\n>>>>>
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn body_region_on_leaf_if_falls_back_to_whole_chunk_python() {
|
||||
// Python `if` inside a function body is a leaf chunk with prologue/epilogue
|
||||
// bytes set by the classifier. Using `~` on it should fall back to
|
||||
// whole-chunk replacement instead of mangling the guard.
|
||||
fn body_region_on_leaf_if_is_rejected_python() {
|
||||
// Python `if` inside a function body is a leaf chunk. Using `~` on it
|
||||
// used to fall back to a whole-chunk replacement and could corrupt
|
||||
// indentation around the guard; it is now rejected with a clear
|
||||
// workaround instead.
|
||||
let source = "def handle(request):\n x = 1\n y = 2\n if request.ok:\n \
|
||||
return \"yes\"\n z = 3\n for item in items:\n process(item)\n \
|
||||
return \"no\"\n";
|
||||
@@ -5287,7 +5539,7 @@ function foo() {\n<<<<<<< HEAD\n\treturn bar();\n=======\n\treturn baz();\n>>>>>
|
||||
.expect("if chunk should exist");
|
||||
assert!(if_chunk.leaf, "if chunk should be leaf");
|
||||
|
||||
let result = apply_edits(&state, &EditParams {
|
||||
let err = apply_edits(&state, &EditParams {
|
||||
operations: vec![EditOperation {
|
||||
op: ChunkEditOp::Put,
|
||||
sel: Some(format!("{}~", if_chunk.path)),
|
||||
@@ -5303,23 +5555,21 @@ function foo() {\n<<<<<<< HEAD\n\treturn bar();\n=======\n\treturn baz();\n>>>>>
|
||||
file_path: "test.py".to_owned(),
|
||||
normalize_indent: None,
|
||||
})
|
||||
.expect("leaf ~ should fall back to whole-chunk, not produce a parse error");
|
||||
.err()
|
||||
.expect("leaf ~ should be rejected");
|
||||
|
||||
assert!(result.parse_valid, "edit should produce valid Python");
|
||||
assert!(
|
||||
result.diff_after.contains("return \"forced\""),
|
||||
"replacement content should appear in output, got: {}",
|
||||
result.diff_after
|
||||
err.contains("Python compound-statement leaf chunks"),
|
||||
"error should identify the unsafe leaf fallback, got: {err}"
|
||||
);
|
||||
assert!(
|
||||
result.diff_after.contains("if request.ok:"),
|
||||
"guard should be preserved (whole-chunk replacement), got: {}",
|
||||
result.diff_after
|
||||
err.contains("parent container's `~`"),
|
||||
"error should mention the parent-body workaround, got: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn body_region_fallback_preserves_head_when_replacement_omits_it_python() {
|
||||
fn body_region_fallback_that_omits_python_head_is_rejected() {
|
||||
let source = "def handle(request):\n x = 1\n y = 2\n if request.ok:\n \
|
||||
return \"yes\"\n z = 3\n for item in items:\n process(item)\n \
|
||||
return \"no\"\n";
|
||||
@@ -5332,7 +5582,7 @@ function foo() {\n<<<<<<< HEAD\n\treturn bar();\n=======\n\treturn baz();\n>>>>>
|
||||
.find(|c| c.kind == ChunkKind::If)
|
||||
.expect("if chunk should exist");
|
||||
|
||||
let result = apply_edits(&state, &EditParams {
|
||||
let err = apply_edits(&state, &EditParams {
|
||||
operations: vec)
|
||||
- Fixed chunk edit path parsing so plan-mode edits to section-addressed `local://PLAN.md:<selector>` paths are classified as writes to the plan file
|
||||
- Fixed SQLite `read` helper queries to reject `where=` clauses with SQL control syntax that could override the structured selector's pagination; raw SQL remains available through `q=SELECT ...`
|
||||
@@ -7158,4 +7163,4 @@ Initial public release.
|
||||
- Git branch display in footer
|
||||
- Message queueing during streaming responses
|
||||
- OAuth integration for Gmail and Google Calendar access
|
||||
- HTML export with syntax highlighting and collapsible sections
|
||||
- HTML export with syntax highlighting and collapsible sections
|
||||
|
||||
@@ -16,6 +16,7 @@ Call format: `{"edits": [{"path": "file:chunk#ID~", "write": "new body"}, …]}`
|
||||
```
|
||||
The tool adds the correct base indent automatically. Never manually pad with the chunk's own indentation.
|
||||
Multiple sibling body lines at the same level all start at column 0: `"print(a)\nprint(b)\nprint(c)\n"`. Only use `\t` when nesting deeper (e.g. `"if cond:\n\tinner\nouter\n"`).
|
||||
Before applying the target's base indent, the tool strips any common leading whitespace shared by all non-empty `write` lines as a safety net. Do not rely on that cleanup for mixed indentation; write `~` bodies at column 0 and use one `\t` per relative nesting level.
|
||||
**Common mistake** when replacing `~` of a function body: do NOT include the function's own indentation.
|
||||
Wrong: `"if b == 0:\n\t\treturn None\n\treturn a / b\n"` — adds the function's base `\t` to every line.
|
||||
Correct: `"if b == 0:\n\treturn None\nreturn a / b\n"` — `if` and `return a / b` at column 0, only `return None` gets `\t` for nesting.
|
||||
@@ -26,8 +27,10 @@ Call format: `{"edits": [{"path": "file:chunk#ID~", "write": "new body"}, …]}`
|
||||
content: "if (x) {\n return true;\n}"
|
||||
```
|
||||
The tool adds the correct base indent automatically, then preserves the tabs/spaces you used inside the snippet. Never manually pad with the chunk's own indentation.
|
||||
Before applying the target's base indent, the tool strips any common leading whitespace shared by all non-empty `write` lines as a safety net. Do not rely on that cleanup for mixed indentation; write `~` bodies at column 0.
|
||||
{{/if}}
|
||||
- Region suffixes only apply to container chunks (classes, functions, impl blocks, sections). On leaf chunks (enum variants, fields, single statements, and compound statements like `if`/`for`/`while`/`match`/`try`), `~` and `^` silently fall back to whole-chunk replacement — prefer the unsuffixed form and always supply the complete replacement (condition + body, not just the body) to avoid dropping structural parts.
|
||||
- Region suffixes only apply to chunks with a real head/body boundary (classes, functions, impl blocks, and similar containers). On code leaf chunks (enum variants, fields, single statements, and compound statements like `if`/`for`/`while`/`match`/`try`), `~` and `^` are rejected. Use the unsuffixed selector and supply the complete replacement content, or edit the parent container's `~` body.
|
||||
- `replace: {old, new}` treats multiline `new` text literally. If `new` contains a newline, include the exact leading whitespace required on every inserted line; it is not stripped and re-indented like `write` content. If `old` starts after existing line indentation and multiline `new` includes that same indentation on its first line, the existing prefix is consumed to avoid double-indenting the first inserted line.
|
||||
- `put`, `find`+`replace`, and `delete` require the current ID. `prepend`/`append` do not.
|
||||
- **IDs change after every edit.** The edit response always carries the new IDs — use those for the next call or run `open(path="file", sel="?")` to refresh. Never reuse an ID from before the latest edit.
|
||||
</rules>
|
||||
@@ -42,14 +45,16 @@ You **MUST** use the narrowest region that covers your change. Putting without a
|
||||
|
||||
<regions>
|
||||
In `read` output, lines marked `^` between the line number and `|` are **head** lines (doc comments, attributes/decorators, signature). Lines without `^` are **body** lines. Use this to decide which region to target:
|
||||
- `fn_foo#ID~` — **body only (the default choice for most edits).** Head lines (`^`) are preserved automatically — doc comments, attributes, and signature stay untouched. On leaf chunks, falls back to whole chunk.
|
||||
- `fn_foo#ID~` — **body only (the default choice for most edits).** Head lines (`^`) are preserved automatically — doc comments, attributes, and signature stay untouched. On code leaf chunks, this is rejected because there is no safe body boundary.
|
||||
- `fn_foo#ID^` — head only (decorators, attributes, doc comments, signature, opening delimiter). Body stays untouched.
|
||||
- `fn_foo#ID` — entire chunk including leading trivia. **You must include doc comments and attributes in `content`; omitting them deletes them.**
|
||||
- `chunk~` + `append`/`prepend` inserts *inside* the container. `chunk` + `append`/`prepend` inserts *outside*.
|
||||
|
||||
**Note on leading trivia:** whether a decorator/doc comment belongs to `^` depends on the parser. In Rust and Python, attributes and decorators are attached to the function chunk, so `^` covers them. In TypeScript/JavaScript, a `@decorator` + `/** jsdoc */` block immediately above a method often surfaces as a **separate sibling chunk** (shown as `chunk#ID` in the `?` listing) rather than as part of the function's `^`. If you need to rewrite a decorator, check the `?` listing for a sibling `chunk#ID` directly above your target.
|
||||
**Note on leading trivia:** whether a decorator/doc comment belongs to `^` depends on the parser. In Rust and Python, attributes and decorators are attached to the function chunk, so `^` covers them. In TypeScript/JavaScript, a `@decorator` + `/** jsdoc */` block immediately above a method often surfaces as a **separate sibling chunk** (shown as `chunk#ID` in the `?` listing) rather than as part of the function's `^`. JSDoc directly above a plain function is more likely to be absorbed into that function's `^`. If you need to rewrite a decorated member, run `open(path="file", sel="?")` and check for a sibling `chunk#ID` directly above your target.
|
||||
|
||||
**Note on non-code formats:** for prose and data formats (markdown, YAML, JSON, fenced code blocks, frontmatter), `^` and `~` fall back to the whole chunk. Always replace the entire chunk and include any delimiter syntax (fence backticks, `---` frontmatter markers, list markers) in your `content` — omitting them deletes them. For markdown sections (`sect_*`), always use unsuffixed whole-chunk replace — `^` and `~` on section containers also fall back to whole-chunk replace. When editing fenced code blocks in markdown, use the exact whitespace from the file (read with `raw` first) — the tool preserves literal indentation inside fenced blocks, but any content you supply is written verbatim. To insert content after a markdown section heading, use `after` on the heading chunk (`sect_*.chunk` or `sect_*.chunk_1`) — not `before`/`prepend` on the section itself, which lands physically before the heading and gets absorbed by the preceding section on reparse.
|
||||
**Python notes:** Python docstrings are body lines, not head lines. A `~` body write on a function that has a docstring deletes the docstring unless you include the docstring in `content`. Python enum members and nested functions/closures are often opaque inside their parent chunk and may not appear as addressable child chunks; use `replace` on the parent chunk or rewrite the parent container body.
|
||||
|
||||
**Note on non-code formats:** for prose and data formats (markdown, YAML, JSON, frontmatter), unsupported `^` and `~` suffixes warn and fall back to whole-chunk editing. Always replace the entire chunk and include any delimiter syntax (fence backticks, `---` frontmatter markers, list markers, table rows, headings) in your `content` — omitting them deletes them. For markdown sections (`sect_*`), prefer unsuffixed whole-chunk replace because `^`/`~` on prose sections can replace the heading too. Fenced code blocks are the exception when the embedded language parser exposes inner chunks; otherwise read with `raw` first and preserve the exact whitespace inside fences. Be cautious appending to markdown tables or lists: if spacing is delicate, rewrite the whole table/list chunk so blank lines and row continuity stay under your control. To insert content after a markdown section heading, use `after` on the heading chunk (`sect_*.chunk` or `sect_*.chunk_1`) — not `before`/`prepend` on the section itself, which lands physically before the heading and gets absorbed by the preceding section on reparse.
|
||||
</regions>
|
||||
|
||||
<ops>
|
||||
@@ -268,6 +273,7 @@ Result — the method (including its doc comment and signature) is removed.
|
||||
- Match the file's real indentation characters in your snippet. The tool preserves your literal tabs/spaces after adding the target region's base indent.
|
||||
{{/if}}
|
||||
- Do NOT include the chunk's base indentation — only indent relative to the region's opening level.
|
||||
- For `write`, the tool strips common leading whitespace shared by all non-empty lines, then adds the target region's base indent. If lines have mixed relative indentation, write them at column 0 so the common-margin cleanup cannot change the structure.
|
||||
- For `~` of a function: write at column 0, and use `\t` for *relative* nesting. Flat body: `"return x;\n"`. Multiple sibling lines: `"print(a)\nprint(b)\nprint(c)\n"` — all at column 0, the tool adds the function's base indent. Nested body: `"if (cond) {\n\treturn x;\n}\n"` — the `if` is at column 0, the `return` is one tab in. Python example — to replace `~` of `def divide(a, b):`, write: `"if b == 0:\n\treturn None\nreturn a / b\n"` — the `if` and `return a / b` are at column 0, `return None` is one `\t` in.
|
||||
- For `^`: write at the chunk's own depth. A class member's head uses `"/// doc\n#[attr]\npub fn start() {"`.
|
||||
{{#if chunkAutoIndent}}
|
||||
@@ -275,5 +281,5 @@ Result — the method (including its doc comment and signature) is removed.
|
||||
{{else}}
|
||||
- For a top-level item: start at zero indent. Write `"fn foo() {\n return 1;\n}\n"`.
|
||||
{{/if}}
|
||||
- The tool strips common leading indentation from your content as a safety net, so accidental over-indentation is corrected.
|
||||
- `replace: {old, new}` does not use the same multiline reindent model. Single-line `new` text inherits the matched line's indentation. Multiline `new` text is inserted literally, so include the exact leading whitespace for every inserted line; when the first replacement line repeats the matched line's existing indentation prefix, the tool consumes that prefix to prevent double indentation.
|
||||
</examples>
|
||||
|
||||
@@ -36,6 +36,11 @@ Chunk reads normalize leading indentation so copied content round-trips cleanly
|
||||
{{else}}
|
||||
Chunk reads preserve literal leading tabs/spaces from the file. When editing, keep the same whitespace characters you see here.
|
||||
{{/if}}
|
||||
`raw` shows the file's literal whitespace. Structured chunk views may normalize or display indentation for edit round-tripping, so use `raw` when exact tabs/spaces matter, especially inside markdown fenced code blocks.
|
||||
|
||||
IDs change after every edit. Use the new IDs from the edit response or refresh with `sel="?"` before the next write.
|
||||
|
||||
Parser boundaries vary by language: TypeScript/JavaScript decorators and JSDoc above decorated methods may appear as sibling `chunk#ID` entries, Python docstrings are body lines, and Python enum members or nested closures may remain opaque inside their parent chunk.
|
||||
|
||||
Chunk trees: JS, TS, TSX, Python, Rust, Go. Others use blank-line fallback.
|
||||
# Inspection
|
||||
|
||||
Reference in New Issue
Block a user