Skip to content

fix(filesystem): indent a flush-left replacement line to match the block - #4823

Open
chrikrah wants to merge 1 commit into
modelcontextprotocol:mainfrom
chrikrah:fix/filesystem-zero-indent
Open

chrikrah wants to merge 1 commit into
modelcontextprotocol:mainfrom
chrikrah:fix/filesystem-zero-indent

Conversation

@chrikrah

Copy link
Copy Markdown

Description

applyFileEdits gates its relative-indent calculation on if (oldIndent && newIndent) at src/filesystem/lib.ts:307. Both are strings, so a line with no leading whitespace makes the gate false, and the code falls through to return line, emitting the replacement line flush left instead of at the block's indentation.

Zero leading whitespace is a known indent of length zero, not a missing one.

Server Details

  • Server: filesystem
  • Changes to: tools (edit_file, through applyFileEdits)

Motivation and Context

Replacing line1\nline2 with lineA\nlineB in a four-space block. On d73f99e:

function foo() {
    lineA
lineB
}

After:

function foo() {
    lineA
    lineB
}

:303 handles the first line separately. Only the lines after it reach the gate.

A flush-left edit is a common paste style. It is also what makes the whitespace-flexible matcher run at all. An oldText that matches the file exactly takes the substring fast path at :280 and never reaches this code.

The offset is unknown only when no old line exists to measure against. That case stays verbatim.

How Has This Been Tested?

$ npx vitest run --run
Test Files  10 passed (10)
     Tests  169 passed (169)

$ npm run build
tsc && shx chmod +x dist/*.js

Revert lib.ts alone, keep the new test, and lib.test.ts gives 1 failed, 56 passed.

Every existing multi-line applyFileEdits test either matches an exact substring, which never reaches this branch, or carries non-empty indentation on every line. This case had no coverage.

Not tested with an LLM client: the assertion is on the bytes applyFileEdits writes, which is where the defect lives.

Breaking Changes

An edit whose replacement lines are flush left now lands indented. That is the correction. A caller relying on the old output was relying on a line losing its indentation.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
    Nothing new. The branch already existed and computed the wrong answer.
  • Breaking change (fix or feature that would cause existing functionality to change)
    See Breaking Changes above.
  • Documentation update
    No document describes the indentation rule.

Checklist

  • I have read the MCP Protocol Documentation
  • My changes follows MCP security best practices
  • I have updated the server's README accordingly
    The README describes edit_file as line-based edits with git-style diff output. It does not say how indentation is derived.
  • I have tested this with an LLM client
    The test asserts on the written bytes directly.
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have documented all environment variables and configuration options
    No environment variable or configuration option changed.

Additional context

Nine open pull requests touch src/filesystem/lib.ts: #4789, #4720, #4692, #4691, #4690, #4689, #4674, #4672 and #4643. None mentions oldIndent, newIndent or relativeIndent. The nearest is #4672, which strips trailing whitespace a few lines above this block.

gh pr list --state all --search "applyFileEdits indentation" returns nothing.

Issue #2034, edit_file failing on whitespace differences, is the matcher finding no match at all. Here the match succeeds and the result is misindented.

applyFileEdits gates its relative-indent calculation on
`if (oldIndent && newIndent)`. Both are strings, so a line with no leading
whitespace makes the gate false and the code falls through to `return line`,
emitting the replacement line flush left instead of at the block's indentation.

Zero leading whitespace is a known indent of length zero, not a missing one.
The offset is only unknown when there is no old line to measure against, which
is now the one case left verbatim.

Before, replacing `line1\nline2` with `lineA\nlineB` in a four-space block:

  function foo() {
      lineA
  lineB
  }

After:

  function foo() {
      lineA
      lineB
  }

Tests: 169 passed across the filesystem server. Reverting lib.ts alone and
keeping the new test gives 1 failed, 56 passed in lib.test.ts.

Every existing multi-line applyFileEdits test either matches an exact substring,
which never reaches this branch, or carries non-empty indentation on every line,
so this case had no coverage.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant