From 1d7eb64b34767123476351b172fadbe52485e9d0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Feb 2026 04:28:40 +0000 Subject: [PATCH 01/11] Fork-specific CI and workflow configurations --- .github/workflows/codacy.yml | 67 ++++++ .github/workflows/maven.yml | 30 +++ .github/workflows/rebase-upstream.yml | 238 +++++++++++++++++++++ .github/workflows/sync-upstream.yml | 289 ++++++++++++++++++++++++++ 4 files changed, 624 insertions(+) create mode 100644 .github/workflows/codacy.yml create mode 100644 .github/workflows/maven.yml create mode 100644 .github/workflows/rebase-upstream.yml create mode 100644 .github/workflows/sync-upstream.yml diff --git a/.github/workflows/codacy.yml b/.github/workflows/codacy.yml new file mode 100644 index 00000000000..649e970f935 --- /dev/null +++ b/.github/workflows/codacy.yml @@ -0,0 +1,67 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# This workflow checks out code, performs a Codacy security scan +# and integrates the results with the +# GitHub Advanced Security code scanning feature. For more information on +# the Codacy security scan action usage and parameters, see +# https://github.com/codacy/codacy-analysis-cli-action. +# For more information on Codacy Analysis CLI in general, see +# https://github.com/codacy/codacy-analysis-cli. + +name: Codacy Security Scan + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '24 8 * * 2' + +permissions: + contents: read + +jobs: + codacy-security-scan: + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + name: Codacy Security Scan + runs-on: ubuntu-latest + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout code + uses: actions/checkout@v4 + + # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis + - name: Run Codacy Analysis CLI + uses: codacy/codacy-analysis-cli-action@d840f886c4bd4edc059706d09c6a1586111c540b +# uses: codacy/codacy-analysis-cli-action@33d455949345bddfdb845fba76b57b70cc83754b + env: + CODEQL_ACTION_EXTRA_OPTIONS: '{"database":{"interpret-results":["--max-paths", 1]}}' + + with: + # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository + # You can also omit the token and run the tools that support default configurations + project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} + verbose: true + output: results.sarif + format: sarif + # Adjust severity of non-security issues + gh-code-scanning-compat: true + # Force 0 exit code to allow SARIF file generation + # This will handover control about PR rejection to the GitHub side + max-allowed-issues: 2147483647 + + + + + # Upload the SARIF file generated in the previous step + - name: Upload SARIF results file + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 00000000000..cbca6233f9b --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,30 @@ +# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Java CI with Maven + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + - name: Set up Maven + uses: stCarolas/setup-maven@d6af6abeda15e98926a57b5aa970a96bb37f97d1 # v5 + with: + maven-version: 3.9.9 + - name: Build with Maven + run: mvn -B package -Pbuild-individual-bundles --file pom.xml diff --git a/.github/workflows/rebase-upstream.yml b/.github/workflows/rebase-upstream.yml new file mode 100644 index 00000000000..0062ddda3ea --- /dev/null +++ b/.github/workflows/rebase-upstream.yml @@ -0,0 +1,238 @@ +name: Rebase Upstream + +on: + issue_comment: + types: [created] + pull_request: + types: [opened] + +jobs: + add-rebase-button: + name: Add Rebase Instructions + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Add comment with rebase instructions + uses: actions/github-script@v8 + with: + script: | + const repoUrl = context.payload.repository.html_url; + const workflowUrl = `${repoUrl}/actions/workflows/rebase-upstream.yml`; + + const comment = `## πŸ”„ Rebase onto Upstream + + To rebase this PR onto the latest \`eclipse-jdt/eclipse.jdt.ui:master\`, comment: + + \`\`\` + /rebase-upstream + \`\`\` + + This will rebase your PR branch onto the upstream repository's master branch. + + πŸ“‹ [View Workflow](${workflowUrl})`; + + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment + }); + + rebase-upstream: + name: Rebase onto Upstream Master + if: | + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + contains(github.event.comment.body, '/rebase-upstream') + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Check user permission + id: check + uses: actions/github-script@v8 + with: + result-encoding: string + script: | + const authorAssociation = context.payload.comment.author_association; + const allowedRoles = ['OWNER', 'MEMBER', 'COLLABORATOR']; + + if (allowedRoles.includes(authorAssociation)) { + return 'true'; + } else { + return 'false'; + } + + - name: Add unauthorized reaction + if: steps.check.outputs.result != 'true' + uses: actions/github-script@v8 + with: + script: | + github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '-1' + }); + + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: '❌ Only repository collaborators can trigger the rebase.' + }); + + - name: Exit if unauthorized + if: steps.check.outputs.result != 'true' + run: exit 1 + + - name: Add rocket reaction + uses: actions/github-script@v8 + with: + script: | + github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'rocket' + }); + + - name: Get PR branch info + id: pr + uses: actions/github-script@v8 + with: + script: | + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number + }); + + core.setOutput('head_ref', pr.head.ref); + core.setOutput('head_repo', pr.head.repo.full_name); + core.setOutput('head_clone_url', pr.head.repo.clone_url); + + // Get the number of commits in the PR + // Note: per_page is set to 250, which covers most PRs. + // For PRs with >250 commits, manual rebasing would be required. + const { data: commits } = await github.rest.pulls.listCommits({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + per_page: 250 + }); + core.setOutput('commit_count', commits.length); + + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + repository: ${{ steps.pr.outputs.head_repo }} + ref: ${{ steps.pr.outputs.head_ref }} + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Add upstream remote and rebase + id: rebase + run: | + git remote add upstream https://github.com/eclipse-jdt/eclipse.jdt.ui.git + git fetch upstream master + + # Get the number of commits in the PR + PR_COMMIT_COUNT=${{ steps.pr.outputs.commit_count }} + echo "PR has $PR_COMMIT_COUNT commits" + + # Validate commit count + if [ "$PR_COMMIT_COUNT" -eq 0 ]; then + echo "Error: PR has 0 commits" + echo "success=false" >> $GITHUB_OUTPUT + exit 0 + fi + + # Rebase only the PR commits onto upstream/master + if git rebase --onto upstream/master HEAD~${PR_COMMIT_COUNT}; then + echo "success=true" >> $GITHUB_OUTPUT + else + echo "success=false" >> $GITHUB_OUTPUT + git rebase --abort + fi + + - name: Push rebased branch + if: steps.rebase.outputs.success == 'true' + run: | + git push --force-with-lease origin ${{ steps.pr.outputs.head_ref }} + + - name: Add success comment + if: steps.rebase.outputs.success == 'true' + uses: actions/github-script@v8 + with: + script: | + github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '+1' + }); + + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: 'βœ… Successfully rebased onto `eclipse-jdt/eclipse.jdt.ui:master`' + }); + + - name: Add failure comment + if: steps.rebase.outputs.success == 'false' + uses: actions/github-script@v8 + with: + script: | + github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '-1' + }); + + const comment = `❌ Rebase failed due to conflicts. + + To resolve manually: + + \`\`\`bash + # Add upstream remote + git remote add upstream https://github.com/eclipse-jdt/eclipse.jdt.ui.git + + # Fetch upstream + git fetch upstream master + + # Count the number of commits in your PR (check GitHub PR page) + # For example, if your PR has 3 commits: + PR_COMMIT_COUNT=3 + + # Rebase only your PR commits onto upstream/master + git rebase --onto upstream/master HEAD~\${PR_COMMIT_COUNT} + + # Resolve conflicts, then: + git add . + git rebase --continue + + # Force push when done + git push --force-with-lease + \`\`\``; + + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment + }); + + - name: Fail workflow if rebase failed + if: steps.rebase.outputs.success == 'false' + run: exit 1 diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 00000000000..d104f59bf67 --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,289 @@ +name: Sync Fork with Upstream + +on: + schedule: + - cron: '0 3 * * *' # Daily at 3:00 AM UTC + workflow_dispatch: + issue_comment: + types: [created] + +jobs: + sync-scheduled: + name: Sync Fork (Scheduled) + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout fork master + uses: actions/checkout@v4 + with: + ref: master + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Add upstream remote + run: | + git remote add upstream https://github.com/eclipse-jdt/eclipse.jdt.ui.git + git fetch upstream master + + - name: Identify and backup fork-specific files + id: backup + run: | + # Create temporary directory for fork-specific files + mkdir -p /tmp/fork-specific + + # Identify fork-specific workflow files (files that don't exist in upstream) + git fetch upstream master + + # Get list of workflow files in fork + FORK_WORKFLOWS=$(git ls-tree -r HEAD --name-only .github/workflows/ 2>/dev/null || echo "") + + # For each workflow file in fork, check if it exists in upstream + for file in $FORK_WORKFLOWS; do + if ! git cat-file -e upstream/master:"$file" 2>/dev/null; then + echo "Fork-specific file: $file" + # Create directory structure and copy file + mkdir -p "/tmp/fork-specific/$(dirname "$file")" + cp "$file" "/tmp/fork-specific/$file" + fi + done + + # Check if any fork-specific files were found + if [ -d "/tmp/fork-specific/.github" ]; then + echo "has_fork_files=true" >> $GITHUB_OUTPUT + echo "Found fork-specific files:" + find /tmp/fork-specific -type f + else + echo "has_fork_files=false" >> $GITHUB_OUTPUT + echo "No fork-specific files found" + fi + + - name: Reset to upstream master + run: | + # Reset fork master to upstream master + git reset --hard upstream/master + + - name: Restore fork-specific files + if: steps.backup.outputs.has_fork_files == 'true' + run: | + # Copy fork-specific files back + if [ -d "/tmp/fork-specific" ]; then + cp -r /tmp/fork-specific/.github ./ 2>/dev/null || true + fi + + # Add all fork-specific changes + git add .github/ + + - name: Commit fork-specific changes + if: steps.backup.outputs.has_fork_files == 'true' + run: | + # Check if there are changes to commit + if ! git diff --cached --quiet; then + git commit -m "Fork-specific CI and workflow configurations" + echo "βœ… Fork-specific changes committed" + else + echo "ℹ️ No fork-specific changes to commit" + fi + + - name: Push to fork master + run: | + git push --force origin master + echo "βœ… Successfully synced with upstream" + + sync-manual: + name: Sync Fork (Manual) + if: | + github.event_name == 'issue_comment' && + contains(github.event.comment.body, '/sync-upstream') + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + steps: + - name: Check user permission + id: check + uses: actions/github-script@v8 + with: + result-encoding: string + script: | + const authorAssociation = context.payload.comment.author_association; + const allowedRoles = ['OWNER', 'MEMBER', 'COLLABORATOR']; + + if (allowedRoles.includes(authorAssociation)) { + return 'true'; + } else { + return 'false'; + } + + - name: Add unauthorized reaction + if: steps.check.outputs.result != 'true' + uses: actions/github-script@v8 + with: + script: | + github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '-1' + }); + + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: '❌ Only repository collaborators can trigger the sync.' + }); + + - name: Exit if unauthorized + if: steps.check.outputs.result != 'true' + run: exit 1 + + - name: Add rocket reaction + uses: actions/github-script@v8 + with: + script: | + github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'rocket' + }); + + - name: Checkout fork master + uses: actions/checkout@v4 + with: + ref: master + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Add upstream remote + run: | + git remote add upstream https://github.com/eclipse-jdt/eclipse.jdt.ui.git + git fetch upstream master + + - name: Identify and backup fork-specific files + id: backup + run: | + # Create temporary directory for fork-specific files + mkdir -p /tmp/fork-specific + + # Identify fork-specific workflow files (files that don't exist in upstream) + git fetch upstream master + + # Get list of workflow files in fork + FORK_WORKFLOWS=$(git ls-tree -r HEAD --name-only .github/workflows/ 2>/dev/null || echo "") + + # For each workflow file in fork, check if it exists in upstream + for file in $FORK_WORKFLOWS; do + if ! git cat-file -e upstream/master:"$file" 2>/dev/null; then + echo "Fork-specific file: $file" + # Create directory structure and copy file + mkdir -p "/tmp/fork-specific/$(dirname "$file")" + cp "$file" "/tmp/fork-specific/$file" + fi + done + + # Check if any fork-specific files were found + if [ -d "/tmp/fork-specific/.github" ]; then + echo "has_fork_files=true" >> $GITHUB_OUTPUT + echo "Found fork-specific files:" + find /tmp/fork-specific -type f + else + echo "has_fork_files=false" >> $GITHUB_OUTPUT + echo "No fork-specific files found" + fi + + - name: Reset to upstream master + id: reset + run: | + # Reset fork master to upstream master + git reset --hard upstream/master + echo "success=true" >> $GITHUB_OUTPUT + + - name: Restore fork-specific files + if: steps.backup.outputs.has_fork_files == 'true' + run: | + # Copy fork-specific files back + if [ -d "/tmp/fork-specific" ]; then + cp -r /tmp/fork-specific/.github ./ 2>/dev/null || true + fi + + # Add all fork-specific changes + git add .github/ + + - name: Commit fork-specific changes + if: steps.backup.outputs.has_fork_files == 'true' + run: | + # Check if there are changes to commit + if ! git diff --cached --quiet; then + git commit -m "Fork-specific CI and workflow configurations" + echo "βœ… Fork-specific changes committed" + else + echo "ℹ️ No fork-specific changes to commit" + fi + + - name: Push to fork master + id: push + run: | + if git push --force origin master; then + echo "success=true" >> $GITHUB_OUTPUT + else + echo "success=false" >> $GITHUB_OUTPUT + fi + + - name: Add success comment + if: steps.push.outputs.success == 'true' + uses: actions/github-script@v8 + with: + script: | + github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '+1' + }); + + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: 'βœ… Successfully synced fork master with `eclipse-jdt/eclipse.jdt.ui:master`' + }); + + - name: Add failure comment + if: steps.push.outputs.success == 'false' + uses: actions/github-script@v8 + with: + script: | + github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '-1' + }); + + const comment = `❌ Sync failed. Please check the workflow logs for details. + + [View workflow run](${context.payload.repository.html_url}/actions/runs/${context.runId})`; + + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment + }); + + - name: Fail workflow if push failed + if: steps.push.outputs.success == 'false' + run: exit 1 From a8ad019a6b7ca86ee9cacbfabe471882125d56f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 20:39:27 +0000 Subject: [PATCH 02/11] Implement DocumentDirtyTracker and integrate with CleanUpPostSaveListener - Created DocumentDirtyTracker to track dirty lines internally and prevent race conditions - Modified CleanUpPostSaveListener to use DocumentDirtyTracker for region calculation - Added defensive bounds checking in validateRegions method - Initialized tracker in CompilationUnitDocumentProvider.connect() Co-authored-by: carstenartur <3164220+carstenartur@users.noreply.github.com> --- .../corext/fix/CleanUpPostSaveListener.java | 85 ++++++- .../CompilationUnitDocumentProvider.java | 7 + .../ui/javaeditor/DocumentDirtyTracker.java | 238 ++++++++++++++++++ 3 files changed, 326 insertions(+), 4 deletions(-) create mode 100644 org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java diff --git a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java index 53cb2de4b43..ae3958f189c 100644 --- a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java +++ b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java @@ -105,6 +105,7 @@ import org.eclipse.jdt.internal.ui.dialogs.OptionalMessageDialog; import org.eclipse.jdt.internal.ui.fix.IMultiLineCleanUp.MultiLineCleanUpContext; import org.eclipse.jdt.internal.ui.fix.MapCleanUpOptions; +import org.eclipse.jdt.internal.ui.javaeditor.DocumentDirtyTracker; import org.eclipse.jdt.internal.ui.javaeditor.saveparticipant.IPostSaveListener; import org.eclipse.jdt.internal.ui.javaeditor.saveparticipant.SaveParticipantPreferenceConfigurationConstants; import org.eclipse.jdt.internal.ui.preferences.BulletListBlock; @@ -330,6 +331,21 @@ public void saved(ICompilationUnit unit, IRegion[] changedRegions, IProgressMoni long oldFileValue= unit.getResource().getModificationStamp(); long oldDocValue= getDocumentStamp((IFile)unit.getResource(), Progress.subMonitor(monitor, 2)); + // Use DocumentDirtyTracker to get current dirty regions instead of stale changedRegions + // This prevents race conditions where regions become invalid between calculation and use + IRegion[] regionsToFormat = changedRegions; + if (requiresChangedRegions(cleanUps)) { + IDocument document = getDocument(unit); + if (document != null) { + DocumentDirtyTracker tracker = DocumentDirtyTracker.get(document); + IRegion[] dirtyRegions = tracker.getDirtyRegions(); + if (dirtyRegions != null) { + // Validate regions before using them + regionsToFormat = validateRegions(dirtyRegions, document); + } + } + } + CompositeChange result= new CompositeChange(FixMessages.CleanUpPostSaveListener_SaveAction_ChangeName); LinkedList undoEdits= new LinkedList<>(); @@ -379,10 +395,10 @@ public void saved(ICompilationUnit unit, IRegion[] changedRegions, IProgressMoni } CleanUpContext context; - if (changedRegions == null) { + if (regionsToFormat == null) { context= new CleanUpContext(unit, ast); } else { - context= new MultiLineCleanUpContext(unit, ast, changedRegions); + context= new MultiLineCleanUpContext(unit, ast, regionsToFormat); } ArrayList undoneCleanUps= new ArrayList<>(); @@ -406,8 +422,8 @@ public void saved(ICompilationUnit unit, IRegion[] changedRegions, IProgressMoni PerformChangeOperation performChangeOperation= new PerformChangeOperation(change); performChangeOperation.setSchedulingRule(unit.getSchedulingRule()); - if (changedRegions != null && changedRegions.length > 0 && requiresChangedRegions(cleanUps)) { - changedRegions= performWithChangedRegionUpdate(performChangeOperation, changedRegions, unit, Progress.subMonitor(monitor, 5)); + if (regionsToFormat != null && regionsToFormat.length > 0 && requiresChangedRegions(cleanUps)) { + regionsToFormat= performWithChangedRegionUpdate(performChangeOperation, regionsToFormat, unit, Progress.subMonitor(monitor, 5)); } else { performChangeOperation.run(Progress.subMonitor(monitor, 5)); } @@ -417,6 +433,15 @@ public void saved(ICompilationUnit unit, IRegion[] changedRegions, IProgressMoni } } while (cleanUps.length > 0); success= true; + + // Clear dirty lines after successful formatting + if (success && requiresChangedRegions(cleanUps)) { + IDocument document = getDocument(unit); + if (document != null) { + DocumentDirtyTracker tracker = DocumentDirtyTracker.get(document); + tracker.clearDirtyLines(); + } + } } finally { manager.changePerformed(result, success); } @@ -655,6 +680,58 @@ private CoreException wrapBadPositionCategoryException(BadPositionCategoryExcept return new CoreException(new Status(IStatus.ERROR, JavaUI.ID_PLUGIN, 0, message, e)); } + /** + * Gets the document for the given compilation unit. + * + * @param unit the compilation unit + * @return the document, or null if not available + * @throws CoreException if an error occurs + */ + private IDocument getDocument(ICompilationUnit unit) throws CoreException { + final ITextFileBufferManager manager= FileBuffers.getTextFileBufferManager(); + final IPath path= unit.getResource().getFullPath(); + + ITextFileBuffer buffer= null; + try { + manager.connect(path, LocationKind.IFILE, new NullProgressMonitor()); + buffer= manager.getTextFileBuffer(path, LocationKind.IFILE); + return buffer != null ? buffer.getDocument() : null; + } finally { + if (buffer != null) + manager.disconnect(path, LocationKind.IFILE, new NullProgressMonitor()); + } + } + + /** + * Validates regions against the current document state, filtering out any invalid regions. + * This provides defensive bounds checking to prevent StringIndexOutOfBoundsException. + * + * @param regions the regions to validate + * @param document the document to validate against + * @return the validated regions, or null if all regions are invalid + */ + private IRegion[] validateRegions(IRegion[] regions, IDocument document) { + if (regions == null || regions.length == 0 || document == null) { + return regions; + } + + ArrayList validRegions = new ArrayList<>(); + int docLength = document.getLength(); + + for (IRegion region : regions) { + // Check bounds: offset and length must be valid + if (region != null && + region.getOffset() >= 0 && + region.getOffset() <= docLength && + region.getLength() >= 0 && + region.getOffset() + region.getLength() <= docLength) { + validRegions.add(region); + } + } + + return validRegions.isEmpty() ? null : validRegions.toArray(new IRegion[validRegions.size()]); + } + private void showSlowCleanUpsWarning(HashSet slowCleanUps) { final StringBuilder cleanUpNames= new StringBuilder(); diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/CompilationUnitDocumentProvider.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/CompilationUnitDocumentProvider.java index 36c5acbd317..929802da8f8 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/CompilationUnitDocumentProvider.java +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/CompilationUnitDocumentProvider.java @@ -1250,6 +1250,13 @@ protected void disposeFileInfo(Object element, FileInfo info) { @Override public void connect(Object element) throws CoreException { super.connect(element); + + // Initialize DocumentDirtyTracker for the document + IDocument document= getDocument(element); + if (document != null) { + DocumentDirtyTracker.get(document); + } + if (getFileInfo(element) != null) return; diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java new file mode 100644 index 00000000000..f6a2328ee9a --- /dev/null +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java @@ -0,0 +1,238 @@ +/******************************************************************************* + * Copyright (c) 2025 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.internal.ui.javaeditor; + +import java.util.ArrayList; +import java.util.List; +import java.util.TreeSet; +import java.util.WeakHashMap; + +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IDocumentListener; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.Region; + +/** + * Tracks dirty (modified) lines in a document to support "format edited lines" functionality. + * This class maintains a set of line numbers that have been modified since the last format/save, + * automatically adjusting them when lines are inserted or deleted to prevent race conditions. + * + * @since 3.40 + */ +public class DocumentDirtyTracker implements IDocumentListener { + + /** WeakHashMap to associate trackers with documents without requiring API changes */ + private static final WeakHashMap trackers = new WeakHashMap<>(); + + /** Set of dirty line numbers, maintained in sorted order */ + private final TreeSet dirtyLines = new TreeSet<>(); + + /** The document being tracked */ + private final IDocument document; + + /** + * Gets or creates a DocumentDirtyTracker for the given document. + * + * @param document the document to track + * @return the tracker for this document + */ + public static synchronized DocumentDirtyTracker get(IDocument document) { + if (document == null) { + throw new IllegalArgumentException("Document cannot be null"); //$NON-NLS-1$ + } + return trackers.computeIfAbsent(document, DocumentDirtyTracker::new); + } + + /** + * Private constructor - use {@link #get(IDocument)} instead. + * + * @param document the document to track + */ + private DocumentDirtyTracker(IDocument document) { + this.document = document; + document.addDocumentListener(this); + } + + /** + * Returns the dirty regions (ranges of consecutive dirty lines). + * This method is safe to call concurrently and returns regions that are + * always valid for the current document state. + * + * @return array of regions representing dirty lines, or null if no lines are dirty + */ + public synchronized IRegion[] getDirtyRegions() { + if (dirtyLines.isEmpty()) { + return null; + } + + List regions = new ArrayList<>(); + Integer startLine = null; + Integer previousLine = null; + + for (Integer line : dirtyLines) { + if (startLine == null) { + // Start of a new region + startLine = line; + previousLine = line; + } else if (line == previousLine + 1) { + // Consecutive line - extend the current region + previousLine = line; + } else { + // Gap found - close current region and start a new one + try { + regions.add(createRegion(startLine, previousLine)); + } catch (BadLocationException e) { + // Line was deleted or invalid - skip this region + } + startLine = line; + previousLine = line; + } + } + + // Add the last region + if (startLine != null) { + try { + regions.add(createRegion(startLine, previousLine)); + } catch (BadLocationException e) { + // Line was deleted or invalid - skip this region + } + } + + return regions.isEmpty() ? null : regions.toArray(new IRegion[regions.size()]); + } + + /** + * Creates a region from a range of line numbers. + * + * @param startLine the first line (inclusive) + * @param endLine the last line (inclusive) + * @return the region covering these lines + * @throws BadLocationException if the lines are invalid + */ + private IRegion createRegion(int startLine, int endLine) throws BadLocationException { + IRegion startLineInfo = document.getLineInformation(startLine); + IRegion endLineInfo = document.getLineInformation(endLine); + + int offset = startLineInfo.getOffset(); + int length = endLineInfo.getOffset() + endLineInfo.getLength() - offset; + + return new Region(offset, length); + } + + /** + * Clears all dirty line markers. Should be called after successful formatting. + */ + public synchronized void clearDirtyLines() { + dirtyLines.clear(); + } + + /** + * Marks specific lines as dirty. + * + * @param lines the line numbers to mark as dirty + */ + public synchronized void markLinesDirty(int... lines) { + for (int line : lines) { + if (line >= 0) { + dirtyLines.add(line); + } + } + } + + @Override + public synchronized void documentAboutToBeChanged(DocumentEvent event) { + // Nothing to do before change + } + + @Override + public synchronized void documentChanged(DocumentEvent event) { + try { + // Get the line numbers affected by this change + int offset = event.getOffset(); + int length = event.getLength(); + String text = event.getText(); + + int startLine = document.getLineOfOffset(offset); + int linesAdded = text != null ? countLines(text) : 0; + int linesRemoved = length > 0 ? document.getLineOfOffset(offset + length) - startLine : 0; + + // Mark changed lines as dirty + for (int i = 0; i <= linesAdded; i++) { + dirtyLines.add(startLine + i); + } + + // Adjust line numbers if lines were added or removed + int netChange = linesAdded - linesRemoved; + if (netChange != 0) { + adjustLineNumbers(startLine + 1, netChange); + } + + } catch (BadLocationException e) { + // If we can't determine the line, ignore this change + } + } + + /** + * Adjusts line numbers after a given line when lines are inserted or deleted. + * + * @param afterLine the line after which to adjust + * @param delta the number of lines added (positive) or removed (negative) + */ + private void adjustLineNumbers(int afterLine, int delta) { + // Get all lines after the change point + TreeSet linesToAdjust = new TreeSet<>(dirtyLines.tailSet(afterLine)); + + // Remove and re-add with adjusted line numbers + dirtyLines.removeAll(linesToAdjust); + for (Integer line : linesToAdjust) { + int newLine = line + delta; + if (newLine >= 0) { + dirtyLines.add(newLine); + } + } + } + + /** + * Counts the number of newline characters in a string. + * + * @param text the text to analyze + * @return the number of newlines (0 if text is null or empty) + */ + private int countLines(String text) { + if (text == null || text.isEmpty()) { + return 0; + } + + int count = 0; + for (int i = 0; i < text.length(); i++) { + if (text.charAt(i) == '\n') { + count++; + } + } + return count; + } + + /** + * Removes the tracker from the document (cleanup). + * Should be called when the document is no longer needed. + */ + public synchronized void dispose() { + document.removeDocumentListener(this); + synchronized (DocumentDirtyTracker.class) { + trackers.remove(document); + } + } +} From ded7db750eed7de9fe0351065bf63447dc0f7792 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 20:41:04 +0000 Subject: [PATCH 03/11] Add comprehensive tests for DocumentDirtyTracker - Created DocumentDirtyTrackerTest with 14 test cases - Tests cover single/multiple edits, line insertion/deletion, UTF-8 handling - Tests verify region bounds validation and concurrent document tracking - Added test to JdtTextTestSuite Co-authored-by: carstenartur <3164220+carstenartur@users.noreply.github.com> --- .../text/tests/DocumentDirtyTrackerTest.java | 255 ++++++++++++++++++ .../jdt/text/tests/JdtTextTestSuite.java | 1 + 2 files changed, 256 insertions(+) create mode 100644 org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java diff --git a/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java b/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java new file mode 100644 index 00000000000..6fe35c62080 --- /dev/null +++ b/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java @@ -0,0 +1,255 @@ +/******************************************************************************* + * Copyright (c) 2025 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.text.tests; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; + +import org.eclipse.jdt.internal.ui.javaeditor.DocumentDirtyTracker; + +/** + * Tests for DocumentDirtyTracker to ensure it correctly tracks dirty lines + * and prevents race conditions in format-on-save operations. + */ +public class DocumentDirtyTrackerTest { + + private IDocument document; + private DocumentDirtyTracker tracker; + + @Before + public void setUp() { + document = new Document(); + tracker = DocumentDirtyTracker.get(document); + } + + @After + public void tearDown() { + if (tracker != null) { + tracker.dispose(); + } + } + + @Test + public void testInitiallyNoDirtyRegions() { + IRegion[] regions = tracker.getDirtyRegions(); + assertNull("Should have no dirty regions initially", regions); + } + + @Test + public void testSingleLineEdit() { + document.set("line1\nline2\nline3\n"); + tracker.clearDirtyLines(); // Clear initial dirty marks + + // Edit line 1 + document.set("modified1\nline2\nline3\n"); + + IRegion[] regions = tracker.getDirtyRegions(); + assertNotNull("Should have dirty regions after edit", regions); + assertEquals("Should have 1 dirty region", 1, regions.length); + } + + @Test + public void testMultipleConsecutiveLines() { + document.set("line1\nline2\nline3\nline4\n"); + tracker.clearDirtyLines(); + + // Edit lines 1 and 2 + document.set("modified1\nmodified2\nline3\nline4\n"); + + IRegion[] regions = tracker.getDirtyRegions(); + assertNotNull("Should have dirty regions", regions); + assertEquals("Should merge consecutive lines into 1 region", 1, regions.length); + } + + @Test + public void testNonConsecutiveLines() { + document.set("line1\nline2\nline3\nline4\n"); + tracker.clearDirtyLines(); + + // Mark lines 0 and 2 as dirty manually + tracker.markLinesDirty(0, 2); + + IRegion[] regions = tracker.getDirtyRegions(); + assertNotNull("Should have dirty regions", regions); + assertEquals("Should have 2 separate regions", 2, regions.length); + } + + @Test + public void testLineInsertion() { + document.set("line1\nline2\nline3\n"); + tracker.clearDirtyLines(); + + // Mark line 1 as dirty + tracker.markLinesDirty(1); + + // Insert a line before line 1 + document.set("line1\ninserted\nline2\nline3\n"); + + // The dirty line should have shifted from 1 to 2 + IRegion[] regions = tracker.getDirtyRegions(); + assertNotNull("Should still have dirty regions after insertion", regions); + } + + @Test + public void testLineDeletion() { + document.set("line1\nline2\nline3\nline4\n"); + tracker.clearDirtyLines(); + + // Mark line 2 as dirty + tracker.markLinesDirty(2); + + // Delete line 1 + document.set("line1\nline3\nline4\n"); + + // The dirty line should have shifted from 2 to 1 + IRegion[] regions = tracker.getDirtyRegions(); + assertNotNull("Should still have dirty regions after deletion", regions); + } + + @Test + public void testClearDirtyLines() { + document.set("line1\nline2\nline3\n"); + tracker.clearDirtyLines(); + + // Edit a line + document.set("modified1\nline2\nline3\n"); + + IRegion[] regions = tracker.getDirtyRegions(); + assertNotNull("Should have dirty regions before clear", regions); + + // Clear dirty lines + tracker.clearDirtyLines(); + + regions = tracker.getDirtyRegions(); + assertNull("Should have no dirty regions after clear", regions); + } + + @Test + public void testUTF8Characters() { + // Test with UTF-8 characters including emojis + document.set("Hello δΈ–η•Œ\nδ½ ε₯½ World\nEmoji πŸ˜€πŸŽ‰\n"); + tracker.clearDirtyLines(); + + // Edit the emoji line + document.set("Hello δΈ–η•Œ\nδ½ ε₯½ World\nModified πŸš€βœ¨\n"); + + IRegion[] regions = tracker.getDirtyRegions(); + assertNotNull("Should handle UTF-8 characters correctly", regions); + assertEquals("Should have 1 dirty region", 1, regions.length); + } + + @Test + public void testRapidSuccessiveEdits() { + document.set("line1\nline2\nline3\n"); + tracker.clearDirtyLines(); + + // Simulate rapid successive edits on different lines + document.set("mod1\nline2\nline3\n"); + document.set("mod1\nmod2\nline3\n"); + document.set("mod1\nmod2\nmod3\n"); + + IRegion[] regions = tracker.getDirtyRegions(); + assertNotNull("Should track all rapid edits", regions); + // All lines should be marked as dirty + assertEquals("All consecutive lines should be in 1 region", 1, regions.length); + } + + @Test + public void testEmptyDocument() { + document.set(""); + tracker.clearDirtyLines(); + + IRegion[] regions = tracker.getDirtyRegions(); + assertNull("Empty document should have no dirty regions", regions); + } + + @Test + public void testSingleLineDocument() { + document.set("single line"); + tracker.clearDirtyLines(); + + // Edit the single line + document.set("modified line"); + + IRegion[] regions = tracker.getDirtyRegions(); + assertNotNull("Should have dirty regions for single line edit", regions); + assertEquals("Should have 1 dirty region", 1, regions.length); + } + + @Test + public void testRegionBounds() { + document.set("line1\nline2\nline3\n"); + tracker.clearDirtyLines(); + + // Edit line 1 + document.set("modified1\nline2\nline3\n"); + + IRegion[] regions = tracker.getDirtyRegions(); + assertNotNull("Should have regions", regions); + + // Verify region is within document bounds + for (IRegion region : regions) { + int offset = region.getOffset(); + int length = region.getLength(); + int docLength = document.getLength(); + + assertEquals("Offset should be non-negative", true, offset >= 0); + assertEquals("Length should be non-negative", true, length >= 0); + assertEquals("Region should be within document bounds", true, offset + length <= docLength); + } + } + + @Test + public void testMultipleDocuments() { + // Test that different documents have independent trackers + IDocument doc1 = new Document("doc1 line1\ndoc1 line2\n"); + IDocument doc2 = new Document("doc2 line1\ndoc2 line2\n"); + + DocumentDirtyTracker tracker1 = DocumentDirtyTracker.get(doc1); + DocumentDirtyTracker tracker2 = DocumentDirtyTracker.get(doc2); + + tracker1.clearDirtyLines(); + tracker2.clearDirtyLines(); + + // Edit only doc1 + doc1.set("doc1 modified\ndoc1 line2\n"); + + IRegion[] regions1 = tracker1.getDirtyRegions(); + IRegion[] regions2 = tracker2.getDirtyRegions(); + + assertNotNull("Doc1 should have dirty regions", regions1); + assertNull("Doc2 should not have dirty regions", regions2); + + tracker1.dispose(); + tracker2.dispose(); + } + + @Test + public void testSameDocumentReturnsSameTracker() { + DocumentDirtyTracker tracker1 = DocumentDirtyTracker.get(document); + DocumentDirtyTracker tracker2 = DocumentDirtyTracker.get(document); + + assertEquals("Same document should return same tracker instance", tracker1, tracker2); + } +} diff --git a/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/JdtTextTestSuite.java b/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/JdtTextTestSuite.java index ca28aa2ca08..276d2dd4b8f 100644 --- a/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/JdtTextTestSuite.java +++ b/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/JdtTextTestSuite.java @@ -35,6 +35,7 @@ @SelectClasses({ PluginsNotLoadedTest.class, CompilationUnitDocumentProviderTest.class, + DocumentDirtyTrackerTest.class, JavaHeuristicScannerTest.class, JavaAutoIndentStrategyTest.class, JavaBreakIteratorTest.class, From 977679caee9e76357164a34cef4917c98e6c233e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 20:42:56 +0000 Subject: [PATCH 04/11] Fix code review issues in DocumentDirtyTracker - Fixed linesRemoved calculation by using documentAboutToBeChanged to capture state before modification - Changed dirty line marking to only mark start and end lines, not all intermediate lines - Improved bounds validation to properly handle empty regions at document end - Changed test assertions from assertEquals to assertTrue for better readability Co-authored-by: carstenartur <3164220+carstenartur@users.noreply.github.com> --- .../text/tests/DocumentDirtyTrackerTest.java | 7 ++-- .../corext/fix/CleanUpPostSaveListener.java | 6 ++-- .../ui/javaeditor/DocumentDirtyTracker.java | 32 +++++++++++++++---- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java b/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java index 6fe35c62080..2343db0c08d 100644 --- a/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java +++ b/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java @@ -17,6 +17,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import org.junit.After; import org.junit.Before; @@ -214,9 +215,9 @@ public void testRegionBounds() { int length = region.getLength(); int docLength = document.getLength(); - assertEquals("Offset should be non-negative", true, offset >= 0); - assertEquals("Length should be non-negative", true, length >= 0); - assertEquals("Region should be within document bounds", true, offset + length <= docLength); + assertTrue("Offset should be non-negative", offset >= 0); + assertTrue("Length should be non-negative", length >= 0); + assertTrue("Region should be within document bounds", offset + length <= docLength); } } diff --git a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java index ae3958f189c..93730ccdf41 100644 --- a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java +++ b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java @@ -720,11 +720,13 @@ private IRegion[] validateRegions(IRegion[] regions, IDocument document) { for (IRegion region : regions) { // Check bounds: offset and length must be valid + // Offset must be within document (< docLength for non-empty regions, <= for empty) + // Empty regions at end of document are allowed if length is 0 if (region != null && region.getOffset() >= 0 && - region.getOffset() <= docLength && region.getLength() >= 0 && - region.getOffset() + region.getLength() <= docLength) { + region.getOffset() + region.getLength() <= docLength && + (region.getLength() > 0 ? region.getOffset() < docLength : region.getOffset() <= docLength)) { validRegions.add(region); } } diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java index f6a2328ee9a..51f1cdb96cb 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java @@ -152,9 +152,25 @@ public synchronized void markLinesDirty(int... lines) { } } + /** Stores the state before a change to calculate removed lines correctly */ + private int lineCountBeforeChange = -1; + @Override public synchronized void documentAboutToBeChanged(DocumentEvent event) { - // Nothing to do before change + // Store the number of lines that will be removed before the change is applied + try { + int offset = event.getOffset(); + int length = event.getLength(); + if (length > 0) { + int startLine = document.getLineOfOffset(offset); + int endLine = document.getLineOfOffset(offset + length); + lineCountBeforeChange = endLine - startLine; + } else { + lineCountBeforeChange = 0; + } + } catch (BadLocationException e) { + lineCountBeforeChange = 0; + } } @Override @@ -162,16 +178,17 @@ public synchronized void documentChanged(DocumentEvent event) { try { // Get the line numbers affected by this change int offset = event.getOffset(); - int length = event.getLength(); String text = event.getText(); int startLine = document.getLineOfOffset(offset); int linesAdded = text != null ? countLines(text) : 0; - int linesRemoved = length > 0 ? document.getLineOfOffset(offset + length) - startLine : 0; + int linesRemoved = lineCountBeforeChange >= 0 ? lineCountBeforeChange : 0; - // Mark changed lines as dirty - for (int i = 0; i <= linesAdded; i++) { - dirtyLines.add(startLine + i); + // Mark the start line as dirty (where the edit occurred) + dirtyLines.add(startLine); + // If newlines were added, also mark the end line as dirty + if (linesAdded > 0) { + dirtyLines.add(startLine + linesAdded); } // Adjust line numbers if lines were added or removed @@ -179,9 +196,12 @@ public synchronized void documentChanged(DocumentEvent event) { if (netChange != 0) { adjustLineNumbers(startLine + 1, netChange); } + + lineCountBeforeChange = -1; // Reset for next change } catch (BadLocationException e) { // If we can't determine the line, ignore this change + lineCountBeforeChange = -1; } } From 6e96af274a2a6b06043c9b3da10afdf78e03c82d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 20:45:06 +0000 Subject: [PATCH 05/11] Address final code review feedback - Use Collections.synchronizedMap for thread-safe WeakHashMap - Remove double-locking by simplifying get() and dispose() methods - Use static NullProgressMonitor instance to reduce allocations - Extract isValidRegion() helper method with clear documentation Co-authored-by: carstenartur <3164220+carstenartur@users.noreply.github.com> --- .../corext/fix/CleanUpPostSaveListener.java | 45 ++++++++++++++----- .../ui/javaeditor/DocumentDirtyTracker.java | 14 +++--- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java index 93730ccdf41..0f8448f9d54 100644 --- a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java +++ b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java @@ -306,6 +306,7 @@ public void widgetSelected(SelectionEvent e) { private static final String WARNING_VALUE= "warning"; //$NON-NLS-1$ private static final String ERROR_VALUE= "error"; //$NON-NLS-1$ private static final String CHANGED_REGION_POSITION_CATEGORY= "changed_region_position_category"; //$NON-NLS-1$ + private static final NullProgressMonitor NULL_PROGRESS_MONITOR = new NullProgressMonitor(); private static boolean FIRST_CALL= false; private static boolean FIRST_CALL_DONE= false; @@ -693,12 +694,12 @@ private IDocument getDocument(ICompilationUnit unit) throws CoreException { ITextFileBuffer buffer= null; try { - manager.connect(path, LocationKind.IFILE, new NullProgressMonitor()); + manager.connect(path, LocationKind.IFILE, NULL_PROGRESS_MONITOR); buffer= manager.getTextFileBuffer(path, LocationKind.IFILE); return buffer != null ? buffer.getDocument() : null; } finally { if (buffer != null) - manager.disconnect(path, LocationKind.IFILE, new NullProgressMonitor()); + manager.disconnect(path, LocationKind.IFILE, NULL_PROGRESS_MONITOR); } } @@ -719,14 +720,7 @@ private IRegion[] validateRegions(IRegion[] regions, IDocument document) { int docLength = document.getLength(); for (IRegion region : regions) { - // Check bounds: offset and length must be valid - // Offset must be within document (< docLength for non-empty regions, <= for empty) - // Empty regions at end of document are allowed if length is 0 - if (region != null && - region.getOffset() >= 0 && - region.getLength() >= 0 && - region.getOffset() + region.getLength() <= docLength && - (region.getLength() > 0 ? region.getOffset() < docLength : region.getOffset() <= docLength)) { + if (region != null && isValidRegion(region, docLength)) { validRegions.add(region); } } @@ -734,6 +728,37 @@ private IRegion[] validateRegions(IRegion[] regions, IDocument document) { return validRegions.isEmpty() ? null : validRegions.toArray(new IRegion[validRegions.size()]); } + /** + * Checks if a region is valid for the given document length. + * A region is valid if: + * - Its offset and length are non-negative + * - The region doesn't extend beyond the document bounds + * - For non-empty regions: offset must be within document content (< docLength) + * - For empty regions: offset can be at document end (== docLength) for cursor positioning + * + * @param region the region to validate + * @param docLength the document length + * @return true if the region is valid + */ + private boolean isValidRegion(IRegion region, int docLength) { + int offset = region.getOffset(); + int length = region.getLength(); + + // Basic validity checks + if (offset < 0 || length < 0) { + return false; + } + + // Check that region doesn't extend beyond document + if (offset + length > docLength) { + return false; + } + + // Empty regions at end are valid (for cursor positioning) + // Non-empty regions must start within document content + return length == 0 || offset < docLength; + } + private void showSlowCleanUpsWarning(HashSet slowCleanUps) { final StringBuilder cleanUpNames= new StringBuilder(); diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java index 51f1cdb96cb..6feb4e95b1f 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java @@ -14,7 +14,9 @@ package org.eclipse.jdt.internal.ui.javaeditor; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.TreeSet; import java.util.WeakHashMap; @@ -34,8 +36,8 @@ */ public class DocumentDirtyTracker implements IDocumentListener { - /** WeakHashMap to associate trackers with documents without requiring API changes */ - private static final WeakHashMap trackers = new WeakHashMap<>(); + /** Synchronized map to associate trackers with documents without requiring API changes */ + private static final Map trackers = Collections.synchronizedMap(new WeakHashMap<>()); /** Set of dirty line numbers, maintained in sorted order */ private final TreeSet dirtyLines = new TreeSet<>(); @@ -49,7 +51,7 @@ public class DocumentDirtyTracker implements IDocumentListener { * @param document the document to track * @return the tracker for this document */ - public static synchronized DocumentDirtyTracker get(IDocument document) { + public static DocumentDirtyTracker get(IDocument document) { if (document == null) { throw new IllegalArgumentException("Document cannot be null"); //$NON-NLS-1$ } @@ -249,10 +251,8 @@ private int countLines(String text) { * Removes the tracker from the document (cleanup). * Should be called when the document is no longer needed. */ - public synchronized void dispose() { + public void dispose() { document.removeDocumentListener(this); - synchronized (DocumentDirtyTracker.class) { - trackers.remove(document); - } + trackers.remove(document); } } From 3fee351004519427739d6ec7c5592efc89cc1e40 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 20:46:50 +0000 Subject: [PATCH 06/11] Address third round of code review feedback - Document thread-safety assumption for lineCountBeforeChange (IDocument guarantees serialized changes) - Fix logic to clear dirty lines after finally block, using saved needsChangedRegions flag - Add clarifying comment about DocumentDirtyTracker.get() side effect in CompilationUnitDocumentProvider Co-authored-by: carstenartur <3164220+carstenartur@users.noreply.github.com> --- .../corext/fix/CleanUpPostSaveListener.java | 21 ++++++++++--------- .../CompilationUnitDocumentProvider.java | 4 +++- .../ui/javaeditor/DocumentDirtyTracker.java | 7 ++++++- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java index 0f8448f9d54..dc66da33648 100644 --- a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java +++ b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java @@ -328,6 +328,7 @@ public void saved(ICompilationUnit unit, IRegion[] changedRegions, IProgressMoni return; ICleanUp[] cleanUps= getCleanUps(unit.getJavaProject().getProject()); + boolean needsChangedRegions = requiresChangedRegions(cleanUps); long oldFileValue= unit.getResource().getModificationStamp(); long oldDocValue= getDocumentStamp((IFile)unit.getResource(), Progress.subMonitor(monitor, 2)); @@ -335,7 +336,7 @@ public void saved(ICompilationUnit unit, IRegion[] changedRegions, IProgressMoni // Use DocumentDirtyTracker to get current dirty regions instead of stale changedRegions // This prevents race conditions where regions become invalid between calculation and use IRegion[] regionsToFormat = changedRegions; - if (requiresChangedRegions(cleanUps)) { + if (needsChangedRegions) { IDocument document = getDocument(unit); if (document != null) { DocumentDirtyTracker tracker = DocumentDirtyTracker.get(document); @@ -434,18 +435,18 @@ public void saved(ICompilationUnit unit, IRegion[] changedRegions, IProgressMoni } } while (cleanUps.length > 0); success= true; - - // Clear dirty lines after successful formatting - if (success && requiresChangedRegions(cleanUps)) { - IDocument document = getDocument(unit); - if (document != null) { - DocumentDirtyTracker tracker = DocumentDirtyTracker.get(document); - tracker.clearDirtyLines(); - } - } } finally { manager.changePerformed(result, success); } + + // Clear dirty lines after successful formatting (if we used the tracker) + if (success && needsChangedRegions) { + IDocument document = getDocument(unit); + if (document != null) { + DocumentDirtyTracker tracker = DocumentDirtyTracker.get(document); + tracker.clearDirtyLines(); + } + } if (undoEdits.size() > 0) { UndoEdit[] undoEditArray= undoEdits.toArray(new UndoEdit[undoEdits.size()]); diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/CompilationUnitDocumentProvider.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/CompilationUnitDocumentProvider.java index 929802da8f8..e9250552071 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/CompilationUnitDocumentProvider.java +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/CompilationUnitDocumentProvider.java @@ -1251,7 +1251,9 @@ protected void disposeFileInfo(Object element, FileInfo info) { public void connect(Object element) throws CoreException { super.connect(element); - // Initialize DocumentDirtyTracker for the document + // Initialize DocumentDirtyTracker for the document. + // The get() method has the side effect of creating and registering + // a document listener if one doesn't exist yet. IDocument document= getDocument(element); if (document != null) { DocumentDirtyTracker.get(document); diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java index 6feb4e95b1f..f305f6809d0 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java @@ -154,7 +154,12 @@ public synchronized void markLinesDirty(int... lines) { } } - /** Stores the state before a change to calculate removed lines correctly */ + /** + * Stores the state before a change to calculate removed lines correctly. + * Note: This assumes document changes are serialized (which is guaranteed by IDocument contract). + * The Eclipse document model ensures documentAboutToBeChanged and documentChanged are called + * sequentially for each change, never concurrently. + */ private int lineCountBeforeChange = -1; @Override From 1d8bc87e5bdf166013262d3215f09a9a4f3014b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 21:16:16 +0000 Subject: [PATCH 07/11] Convert DocumentDirtyTrackerTest to JUnit 5 - Changed imports from org.junit.* to org.junit.jupiter.api.* - Removed unused assertArrayEquals import - Changed @Before to @BeforeEach and @After to @AfterEach - Fixed build compilation error Co-authored-by: carstenartur <3164220+carstenartur@users.noreply.github.com> --- .../text/tests/DocumentDirtyTrackerTest.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java b/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java index 2343db0c08d..3ab081ccc0e 100644 --- a/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java +++ b/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java @@ -13,15 +13,14 @@ *******************************************************************************/ package org.eclipse.jdt.text.tests; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; @@ -38,13 +37,13 @@ public class DocumentDirtyTrackerTest { private IDocument document; private DocumentDirtyTracker tracker; - @Before + @BeforeEach public void setUp() { document = new Document(); tracker = DocumentDirtyTracker.get(document); } - @After + @AfterEach public void tearDown() { if (tracker != null) { tracker.dispose(); From a333582202976782e8349aaecd15d00d25a7321a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:59:48 +0000 Subject: [PATCH 08/11] Fix JUnit 5 assertion parameter order - Changed all assertions to use correct JUnit 5 parameter order (value first, message last) - Fixed assertNull, assertNotNull, assertEquals, and assertTrue calls - Resolved build compilation errors Co-authored-by: carstenartur <3164220+carstenartur@users.noreply.github.com> --- .../text/tests/DocumentDirtyTrackerTest.java | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java b/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java index 3ab081ccc0e..fc2a39049d9 100644 --- a/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java +++ b/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java @@ -53,7 +53,7 @@ public void tearDown() { @Test public void testInitiallyNoDirtyRegions() { IRegion[] regions = tracker.getDirtyRegions(); - assertNull("Should have no dirty regions initially", regions); + assertNull(regions, "Should have no dirty regions initially"); } @Test @@ -65,8 +65,8 @@ public void testSingleLineEdit() { document.set("modified1\nline2\nline3\n"); IRegion[] regions = tracker.getDirtyRegions(); - assertNotNull("Should have dirty regions after edit", regions); - assertEquals("Should have 1 dirty region", 1, regions.length); + assertNotNull(regions, "Should have dirty regions after edit"); + assertEquals(1, regions.length, "Should have 1 dirty region"); } @Test @@ -78,8 +78,8 @@ public void testMultipleConsecutiveLines() { document.set("modified1\nmodified2\nline3\nline4\n"); IRegion[] regions = tracker.getDirtyRegions(); - assertNotNull("Should have dirty regions", regions); - assertEquals("Should merge consecutive lines into 1 region", 1, regions.length); + assertNotNull(regions, "Should have dirty regions"); + assertEquals(1, regions.length, "Should merge consecutive lines into 1 region"); } @Test @@ -91,8 +91,8 @@ public void testNonConsecutiveLines() { tracker.markLinesDirty(0, 2); IRegion[] regions = tracker.getDirtyRegions(); - assertNotNull("Should have dirty regions", regions); - assertEquals("Should have 2 separate regions", 2, regions.length); + assertNotNull(regions, "Should have dirty regions"); + assertEquals(2, regions.length, "Should have 2 separate regions"); } @Test @@ -108,7 +108,7 @@ public void testLineInsertion() { // The dirty line should have shifted from 1 to 2 IRegion[] regions = tracker.getDirtyRegions(); - assertNotNull("Should still have dirty regions after insertion", regions); + assertNotNull(regions, "Should still have dirty regions after insertion"); } @Test @@ -124,7 +124,7 @@ public void testLineDeletion() { // The dirty line should have shifted from 2 to 1 IRegion[] regions = tracker.getDirtyRegions(); - assertNotNull("Should still have dirty regions after deletion", regions); + assertNotNull(regions, "Should still have dirty regions after deletion"); } @Test @@ -136,13 +136,13 @@ public void testClearDirtyLines() { document.set("modified1\nline2\nline3\n"); IRegion[] regions = tracker.getDirtyRegions(); - assertNotNull("Should have dirty regions before clear", regions); + assertNotNull(regions, "Should have dirty regions before clear"); // Clear dirty lines tracker.clearDirtyLines(); regions = tracker.getDirtyRegions(); - assertNull("Should have no dirty regions after clear", regions); + assertNull(regions, "Should have no dirty regions after clear"); } @Test @@ -155,8 +155,8 @@ public void testUTF8Characters() { document.set("Hello δΈ–η•Œ\nδ½ ε₯½ World\nModified πŸš€βœ¨\n"); IRegion[] regions = tracker.getDirtyRegions(); - assertNotNull("Should handle UTF-8 characters correctly", regions); - assertEquals("Should have 1 dirty region", 1, regions.length); + assertNotNull(regions, "Should handle UTF-8 characters correctly"); + assertEquals(1, regions.length, "Should have 1 dirty region"); } @Test @@ -170,9 +170,9 @@ public void testRapidSuccessiveEdits() { document.set("mod1\nmod2\nmod3\n"); IRegion[] regions = tracker.getDirtyRegions(); - assertNotNull("Should track all rapid edits", regions); + assertNotNull(regions, "Should track all rapid edits"); // All lines should be marked as dirty - assertEquals("All consecutive lines should be in 1 region", 1, regions.length); + assertEquals(1, regions.length, "All consecutive lines should be in 1 region"); } @Test @@ -181,7 +181,7 @@ public void testEmptyDocument() { tracker.clearDirtyLines(); IRegion[] regions = tracker.getDirtyRegions(); - assertNull("Empty document should have no dirty regions", regions); + assertNull(regions, "Empty document should have no dirty regions"); } @Test @@ -193,8 +193,8 @@ public void testSingleLineDocument() { document.set("modified line"); IRegion[] regions = tracker.getDirtyRegions(); - assertNotNull("Should have dirty regions for single line edit", regions); - assertEquals("Should have 1 dirty region", 1, regions.length); + assertNotNull(regions, "Should have dirty regions for single line edit"); + assertEquals(1, regions.length, "Should have 1 dirty region"); } @Test @@ -206,7 +206,7 @@ public void testRegionBounds() { document.set("modified1\nline2\nline3\n"); IRegion[] regions = tracker.getDirtyRegions(); - assertNotNull("Should have regions", regions); + assertNotNull(regions, "Should have regions"); // Verify region is within document bounds for (IRegion region : regions) { @@ -214,9 +214,9 @@ public void testRegionBounds() { int length = region.getLength(); int docLength = document.getLength(); - assertTrue("Offset should be non-negative", offset >= 0); - assertTrue("Length should be non-negative", length >= 0); - assertTrue("Region should be within document bounds", offset + length <= docLength); + assertTrue(offset >= 0, "Offset should be non-negative"); + assertTrue(length >= 0, "Length should be non-negative"); + assertTrue(offset + length <= docLength, "Region should be within document bounds"); } } @@ -238,8 +238,8 @@ public void testMultipleDocuments() { IRegion[] regions1 = tracker1.getDirtyRegions(); IRegion[] regions2 = tracker2.getDirtyRegions(); - assertNotNull("Doc1 should have dirty regions", regions1); - assertNull("Doc2 should not have dirty regions", regions2); + assertNotNull(regions1, "Doc1 should have dirty regions"); + assertNull(regions2, "Doc2 should not have dirty regions"); tracker1.dispose(); tracker2.dispose(); @@ -250,6 +250,6 @@ public void testSameDocumentReturnsSameTracker() { DocumentDirtyTracker tracker1 = DocumentDirtyTracker.get(document); DocumentDirtyTracker tracker2 = DocumentDirtyTracker.get(document); - assertEquals("Same document should return same tracker instance", tracker1, tracker2); + assertEquals(tracker1, tracker2, "Same document should return same tracker instance"); } } From 9edc80e5d2d64dee1a1987a4b932846261c8ca67 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 02:53:41 +0100 Subject: [PATCH 09/11] Correct copyright attribution in DocumentDirtyTracker files (#77) --- .../org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java | 4 ++-- .../jdt/internal/ui/javaeditor/DocumentDirtyTracker.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java b/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java index fc2a39049d9..647c6a00de4 100644 --- a/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java +++ b/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2025 IBM Corporation and others. + * Copyright (c) 2025 Carsten Hammer and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -9,7 +9,7 @@ * SPDX-License-Identifier: EPL-2.0 * * Contributors: - * IBM Corporation - initial API and implementation + * Carsten Hammer - initial API and implementation (with assistance from GitHub Copilot) *******************************************************************************/ package org.eclipse.jdt.text.tests; diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java index f305f6809d0..6d4f11e4e1d 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/DocumentDirtyTracker.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2025 IBM Corporation and others. + * Copyright (c) 2025 Carsten Hammer and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -9,7 +9,7 @@ * SPDX-License-Identifier: EPL-2.0 * * Contributors: - * IBM Corporation - initial API and implementation + * Carsten Hammer - initial API and implementation (with assistance from GitHub Copilot) *******************************************************************************/ package org.eclipse.jdt.internal.ui.javaeditor; From fdd18a6529543c7ce9346bb2e533fc840f641c9c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 08:23:17 +0100 Subject: [PATCH 10/11] Fix code quality issues in DocumentDirtyTracker: mutable static, redundant I/O, test coverage (#79) --- .../text/tests/DocumentDirtyTrackerTest.java | 56 +++++++++++++++++++ .../corext/fix/CleanUpPostSaveListener.java | 19 +++---- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java b/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java index 647c6a00de4..9d5734cb9a9 100644 --- a/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java +++ b/org.eclipse.jdt.text.tests/src/org/eclipse/jdt/text/tests/DocumentDirtyTrackerTest.java @@ -252,4 +252,60 @@ public void testSameDocumentReturnsSameTracker() { assertEquals(tracker1, tracker2, "Same document should return same tracker instance"); } + + @Test + public void testIncrementalSingleLineEdit() throws Exception { + document.set("line1\nline2\nline3\n"); + tracker.clearDirtyLines(); + + // Incremental edit on line 0: replace "line1" with "modified1" + document.replace(0, 5, "modified1"); + + IRegion[] regions = tracker.getDirtyRegions(); + assertNotNull(regions, "Should have dirty regions after incremental edit"); + assertEquals(1, regions.length, "Should have 1 dirty region"); + } + + @Test + public void testIncrementalInsertNewLine() throws Exception { + document.set("line1\nline2\nline3\n"); + tracker.clearDirtyLines(); + + // Insert a new line after "line1\n" (offset 6) + document.replace(6, 0, "inserted\n"); + + IRegion[] regions = tracker.getDirtyRegions(); + assertNotNull(regions, "Should have dirty regions after line insertion"); + } + + @Test + public void testIncrementalDeleteLine() throws Exception { + document.set("line1\nline2\nline3\n"); + tracker.clearDirtyLines(); + + // Mark line 2 as dirty first + tracker.markLinesDirty(2); + + // Delete "line2\n" (offset 6, length 6) + document.replace(6, 6, ""); + + IRegion[] regions = tracker.getDirtyRegions(); + assertNotNull(regions, "Should still have dirty regions after deletion"); + } + + @Test + public void testIncrementalMultipleEditsOnDifferentLines() throws Exception { + document.set("line1\nline2\nline3\nline4\n"); + tracker.clearDirtyLines(); + + // Edit line 0 + document.replace(0, 5, "mod1"); + // Edit line 2 (offsets shifted because line 0 is now shorter) + int line2Offset = document.getLineOffset(2); + document.replace(line2Offset, 5, "mod3"); + + IRegion[] regions = tracker.getDirtyRegions(); + assertNotNull(regions, "Should have dirty regions"); + assertEquals(2, regions.length, "Should have 2 separate dirty regions for non-consecutive edits"); + } } diff --git a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java index dc66da33648..6627f4141fd 100644 --- a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java +++ b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java @@ -306,7 +306,6 @@ public void widgetSelected(SelectionEvent e) { private static final String WARNING_VALUE= "warning"; //$NON-NLS-1$ private static final String ERROR_VALUE= "error"; //$NON-NLS-1$ private static final String CHANGED_REGION_POSITION_CATEGORY= "changed_region_position_category"; //$NON-NLS-1$ - private static final NullProgressMonitor NULL_PROGRESS_MONITOR = new NullProgressMonitor(); private static boolean FIRST_CALL= false; private static boolean FIRST_CALL_DONE= false; @@ -336,10 +335,12 @@ public void saved(ICompilationUnit unit, IRegion[] changedRegions, IProgressMoni // Use DocumentDirtyTracker to get current dirty regions instead of stale changedRegions // This prevents race conditions where regions become invalid between calculation and use IRegion[] regionsToFormat = changedRegions; + IDocument document = null; + DocumentDirtyTracker tracker = null; if (needsChangedRegions) { - IDocument document = getDocument(unit); + document = getDocument(unit); if (document != null) { - DocumentDirtyTracker tracker = DocumentDirtyTracker.get(document); + tracker = DocumentDirtyTracker.get(document); IRegion[] dirtyRegions = tracker.getDirtyRegions(); if (dirtyRegions != null) { // Validate regions before using them @@ -440,12 +441,8 @@ public void saved(ICompilationUnit unit, IRegion[] changedRegions, IProgressMoni } // Clear dirty lines after successful formatting (if we used the tracker) - if (success && needsChangedRegions) { - IDocument document = getDocument(unit); - if (document != null) { - DocumentDirtyTracker tracker = DocumentDirtyTracker.get(document); - tracker.clearDirtyLines(); - } + if (success && needsChangedRegions && tracker != null) { + tracker.clearDirtyLines(); } if (undoEdits.size() > 0) { @@ -695,12 +692,12 @@ private IDocument getDocument(ICompilationUnit unit) throws CoreException { ITextFileBuffer buffer= null; try { - manager.connect(path, LocationKind.IFILE, NULL_PROGRESS_MONITOR); + manager.connect(path, LocationKind.IFILE, new NullProgressMonitor()); buffer= manager.getTextFileBuffer(path, LocationKind.IFILE); return buffer != null ? buffer.getDocument() : null; } finally { if (buffer != null) - manager.disconnect(path, LocationKind.IFILE, NULL_PROGRESS_MONITOR); + manager.disconnect(path, LocationKind.IFILE, new NullProgressMonitor()); } } From 3eb033470de513269ebc8aa6e9c192a613cad239 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 08:52:05 +0100 Subject: [PATCH 11/11] Remove fork-specific GitHub Actions workflows (#80) --- .github/workflows/codacy.yml | 67 ------ .github/workflows/maven.yml | 30 --- .github/workflows/rebase-upstream.yml | 238 --------------------- .github/workflows/sync-upstream.yml | 289 -------------------------- 4 files changed, 624 deletions(-) delete mode 100644 .github/workflows/codacy.yml delete mode 100644 .github/workflows/maven.yml delete mode 100644 .github/workflows/rebase-upstream.yml delete mode 100644 .github/workflows/sync-upstream.yml diff --git a/.github/workflows/codacy.yml b/.github/workflows/codacy.yml deleted file mode 100644 index 649e970f935..00000000000 --- a/.github/workflows/codacy.yml +++ /dev/null @@ -1,67 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -# This workflow checks out code, performs a Codacy security scan -# and integrates the results with the -# GitHub Advanced Security code scanning feature. For more information on -# the Codacy security scan action usage and parameters, see -# https://github.com/codacy/codacy-analysis-cli-action. -# For more information on Codacy Analysis CLI in general, see -# https://github.com/codacy/codacy-analysis-cli. - -name: Codacy Security Scan - -on: - push: - branches: [ master ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] - schedule: - - cron: '24 8 * * 2' - -permissions: - contents: read - -jobs: - codacy-security-scan: - permissions: - contents: read # for actions/checkout to fetch code - security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - name: Codacy Security Scan - runs-on: ubuntu-latest - steps: - # Checkout the repository to the GitHub Actions runner - - name: Checkout code - uses: actions/checkout@v4 - - # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis - - name: Run Codacy Analysis CLI - uses: codacy/codacy-analysis-cli-action@d840f886c4bd4edc059706d09c6a1586111c540b -# uses: codacy/codacy-analysis-cli-action@33d455949345bddfdb845fba76b57b70cc83754b - env: - CODEQL_ACTION_EXTRA_OPTIONS: '{"database":{"interpret-results":["--max-paths", 1]}}' - - with: - # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository - # You can also omit the token and run the tools that support default configurations - project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} - verbose: true - output: results.sarif - format: sarif - # Adjust severity of non-security issues - gh-code-scanning-compat: true - # Force 0 exit code to allow SARIF file generation - # This will handover control about PR rejection to the GitHub side - max-allowed-issues: 2147483647 - - - - - # Upload the SARIF file generated in the previous step - - name: Upload SARIF results file - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: results.sarif diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml deleted file mode 100644 index cbca6233f9b..00000000000 --- a/.github/workflows/maven.yml +++ /dev/null @@ -1,30 +0,0 @@ -# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time -# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven - -name: Java CI with Maven - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Set up JDK 21 - uses: actions/setup-java@v4 - with: - java-version: '21' - distribution: 'temurin' - cache: maven - - name: Set up Maven - uses: stCarolas/setup-maven@d6af6abeda15e98926a57b5aa970a96bb37f97d1 # v5 - with: - maven-version: 3.9.9 - - name: Build with Maven - run: mvn -B package -Pbuild-individual-bundles --file pom.xml diff --git a/.github/workflows/rebase-upstream.yml b/.github/workflows/rebase-upstream.yml deleted file mode 100644 index 0062ddda3ea..00000000000 --- a/.github/workflows/rebase-upstream.yml +++ /dev/null @@ -1,238 +0,0 @@ -name: Rebase Upstream - -on: - issue_comment: - types: [created] - pull_request: - types: [opened] - -jobs: - add-rebase-button: - name: Add Rebase Instructions - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - permissions: - pull-requests: write - steps: - - name: Add comment with rebase instructions - uses: actions/github-script@v8 - with: - script: | - const repoUrl = context.payload.repository.html_url; - const workflowUrl = `${repoUrl}/actions/workflows/rebase-upstream.yml`; - - const comment = `## πŸ”„ Rebase onto Upstream - - To rebase this PR onto the latest \`eclipse-jdt/eclipse.jdt.ui:master\`, comment: - - \`\`\` - /rebase-upstream - \`\`\` - - This will rebase your PR branch onto the upstream repository's master branch. - - πŸ“‹ [View Workflow](${workflowUrl})`; - - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: comment - }); - - rebase-upstream: - name: Rebase onto Upstream Master - if: | - github.event_name == 'issue_comment' && - github.event.issue.pull_request && - contains(github.event.comment.body, '/rebase-upstream') - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - name: Check user permission - id: check - uses: actions/github-script@v8 - with: - result-encoding: string - script: | - const authorAssociation = context.payload.comment.author_association; - const allowedRoles = ['OWNER', 'MEMBER', 'COLLABORATOR']; - - if (allowedRoles.includes(authorAssociation)) { - return 'true'; - } else { - return 'false'; - } - - - name: Add unauthorized reaction - if: steps.check.outputs.result != 'true' - uses: actions/github-script@v8 - with: - script: | - github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: '-1' - }); - - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: '❌ Only repository collaborators can trigger the rebase.' - }); - - - name: Exit if unauthorized - if: steps.check.outputs.result != 'true' - run: exit 1 - - - name: Add rocket reaction - uses: actions/github-script@v8 - with: - script: | - github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: 'rocket' - }); - - - name: Get PR branch info - id: pr - uses: actions/github-script@v8 - with: - script: | - const { data: pr } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number - }); - - core.setOutput('head_ref', pr.head.ref); - core.setOutput('head_repo', pr.head.repo.full_name); - core.setOutput('head_clone_url', pr.head.repo.clone_url); - - // Get the number of commits in the PR - // Note: per_page is set to 250, which covers most PRs. - // For PRs with >250 commits, manual rebasing would be required. - const { data: commits } = await github.rest.pulls.listCommits({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number, - per_page: 250 - }); - core.setOutput('commit_count', commits.length); - - - name: Checkout PR branch - uses: actions/checkout@v4 - with: - repository: ${{ steps.pr.outputs.head_repo }} - ref: ${{ steps.pr.outputs.head_ref }} - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 - - - name: Configure Git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Add upstream remote and rebase - id: rebase - run: | - git remote add upstream https://github.com/eclipse-jdt/eclipse.jdt.ui.git - git fetch upstream master - - # Get the number of commits in the PR - PR_COMMIT_COUNT=${{ steps.pr.outputs.commit_count }} - echo "PR has $PR_COMMIT_COUNT commits" - - # Validate commit count - if [ "$PR_COMMIT_COUNT" -eq 0 ]; then - echo "Error: PR has 0 commits" - echo "success=false" >> $GITHUB_OUTPUT - exit 0 - fi - - # Rebase only the PR commits onto upstream/master - if git rebase --onto upstream/master HEAD~${PR_COMMIT_COUNT}; then - echo "success=true" >> $GITHUB_OUTPUT - else - echo "success=false" >> $GITHUB_OUTPUT - git rebase --abort - fi - - - name: Push rebased branch - if: steps.rebase.outputs.success == 'true' - run: | - git push --force-with-lease origin ${{ steps.pr.outputs.head_ref }} - - - name: Add success comment - if: steps.rebase.outputs.success == 'true' - uses: actions/github-script@v8 - with: - script: | - github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: '+1' - }); - - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: 'βœ… Successfully rebased onto `eclipse-jdt/eclipse.jdt.ui:master`' - }); - - - name: Add failure comment - if: steps.rebase.outputs.success == 'false' - uses: actions/github-script@v8 - with: - script: | - github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: '-1' - }); - - const comment = `❌ Rebase failed due to conflicts. - - To resolve manually: - - \`\`\`bash - # Add upstream remote - git remote add upstream https://github.com/eclipse-jdt/eclipse.jdt.ui.git - - # Fetch upstream - git fetch upstream master - - # Count the number of commits in your PR (check GitHub PR page) - # For example, if your PR has 3 commits: - PR_COMMIT_COUNT=3 - - # Rebase only your PR commits onto upstream/master - git rebase --onto upstream/master HEAD~\${PR_COMMIT_COUNT} - - # Resolve conflicts, then: - git add . - git rebase --continue - - # Force push when done - git push --force-with-lease - \`\`\``; - - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: comment - }); - - - name: Fail workflow if rebase failed - if: steps.rebase.outputs.success == 'false' - run: exit 1 diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml deleted file mode 100644 index d104f59bf67..00000000000 --- a/.github/workflows/sync-upstream.yml +++ /dev/null @@ -1,289 +0,0 @@ -name: Sync Fork with Upstream - -on: - schedule: - - cron: '0 3 * * *' # Daily at 3:00 AM UTC - workflow_dispatch: - issue_comment: - types: [created] - -jobs: - sync-scheduled: - name: Sync Fork (Scheduled) - if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout fork master - uses: actions/checkout@v4 - with: - ref: master - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Configure Git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Add upstream remote - run: | - git remote add upstream https://github.com/eclipse-jdt/eclipse.jdt.ui.git - git fetch upstream master - - - name: Identify and backup fork-specific files - id: backup - run: | - # Create temporary directory for fork-specific files - mkdir -p /tmp/fork-specific - - # Identify fork-specific workflow files (files that don't exist in upstream) - git fetch upstream master - - # Get list of workflow files in fork - FORK_WORKFLOWS=$(git ls-tree -r HEAD --name-only .github/workflows/ 2>/dev/null || echo "") - - # For each workflow file in fork, check if it exists in upstream - for file in $FORK_WORKFLOWS; do - if ! git cat-file -e upstream/master:"$file" 2>/dev/null; then - echo "Fork-specific file: $file" - # Create directory structure and copy file - mkdir -p "/tmp/fork-specific/$(dirname "$file")" - cp "$file" "/tmp/fork-specific/$file" - fi - done - - # Check if any fork-specific files were found - if [ -d "/tmp/fork-specific/.github" ]; then - echo "has_fork_files=true" >> $GITHUB_OUTPUT - echo "Found fork-specific files:" - find /tmp/fork-specific -type f - else - echo "has_fork_files=false" >> $GITHUB_OUTPUT - echo "No fork-specific files found" - fi - - - name: Reset to upstream master - run: | - # Reset fork master to upstream master - git reset --hard upstream/master - - - name: Restore fork-specific files - if: steps.backup.outputs.has_fork_files == 'true' - run: | - # Copy fork-specific files back - if [ -d "/tmp/fork-specific" ]; then - cp -r /tmp/fork-specific/.github ./ 2>/dev/null || true - fi - - # Add all fork-specific changes - git add .github/ - - - name: Commit fork-specific changes - if: steps.backup.outputs.has_fork_files == 'true' - run: | - # Check if there are changes to commit - if ! git diff --cached --quiet; then - git commit -m "Fork-specific CI and workflow configurations" - echo "βœ… Fork-specific changes committed" - else - echo "ℹ️ No fork-specific changes to commit" - fi - - - name: Push to fork master - run: | - git push --force origin master - echo "βœ… Successfully synced with upstream" - - sync-manual: - name: Sync Fork (Manual) - if: | - github.event_name == 'issue_comment' && - contains(github.event.comment.body, '/sync-upstream') - runs-on: ubuntu-latest - permissions: - contents: write - issues: write - steps: - - name: Check user permission - id: check - uses: actions/github-script@v8 - with: - result-encoding: string - script: | - const authorAssociation = context.payload.comment.author_association; - const allowedRoles = ['OWNER', 'MEMBER', 'COLLABORATOR']; - - if (allowedRoles.includes(authorAssociation)) { - return 'true'; - } else { - return 'false'; - } - - - name: Add unauthorized reaction - if: steps.check.outputs.result != 'true' - uses: actions/github-script@v8 - with: - script: | - github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: '-1' - }); - - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: '❌ Only repository collaborators can trigger the sync.' - }); - - - name: Exit if unauthorized - if: steps.check.outputs.result != 'true' - run: exit 1 - - - name: Add rocket reaction - uses: actions/github-script@v8 - with: - script: | - github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: 'rocket' - }); - - - name: Checkout fork master - uses: actions/checkout@v4 - with: - ref: master - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Configure Git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Add upstream remote - run: | - git remote add upstream https://github.com/eclipse-jdt/eclipse.jdt.ui.git - git fetch upstream master - - - name: Identify and backup fork-specific files - id: backup - run: | - # Create temporary directory for fork-specific files - mkdir -p /tmp/fork-specific - - # Identify fork-specific workflow files (files that don't exist in upstream) - git fetch upstream master - - # Get list of workflow files in fork - FORK_WORKFLOWS=$(git ls-tree -r HEAD --name-only .github/workflows/ 2>/dev/null || echo "") - - # For each workflow file in fork, check if it exists in upstream - for file in $FORK_WORKFLOWS; do - if ! git cat-file -e upstream/master:"$file" 2>/dev/null; then - echo "Fork-specific file: $file" - # Create directory structure and copy file - mkdir -p "/tmp/fork-specific/$(dirname "$file")" - cp "$file" "/tmp/fork-specific/$file" - fi - done - - # Check if any fork-specific files were found - if [ -d "/tmp/fork-specific/.github" ]; then - echo "has_fork_files=true" >> $GITHUB_OUTPUT - echo "Found fork-specific files:" - find /tmp/fork-specific -type f - else - echo "has_fork_files=false" >> $GITHUB_OUTPUT - echo "No fork-specific files found" - fi - - - name: Reset to upstream master - id: reset - run: | - # Reset fork master to upstream master - git reset --hard upstream/master - echo "success=true" >> $GITHUB_OUTPUT - - - name: Restore fork-specific files - if: steps.backup.outputs.has_fork_files == 'true' - run: | - # Copy fork-specific files back - if [ -d "/tmp/fork-specific" ]; then - cp -r /tmp/fork-specific/.github ./ 2>/dev/null || true - fi - - # Add all fork-specific changes - git add .github/ - - - name: Commit fork-specific changes - if: steps.backup.outputs.has_fork_files == 'true' - run: | - # Check if there are changes to commit - if ! git diff --cached --quiet; then - git commit -m "Fork-specific CI and workflow configurations" - echo "βœ… Fork-specific changes committed" - else - echo "ℹ️ No fork-specific changes to commit" - fi - - - name: Push to fork master - id: push - run: | - if git push --force origin master; then - echo "success=true" >> $GITHUB_OUTPUT - else - echo "success=false" >> $GITHUB_OUTPUT - fi - - - name: Add success comment - if: steps.push.outputs.success == 'true' - uses: actions/github-script@v8 - with: - script: | - github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: '+1' - }); - - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: 'βœ… Successfully synced fork master with `eclipse-jdt/eclipse.jdt.ui:master`' - }); - - - name: Add failure comment - if: steps.push.outputs.success == 'false' - uses: actions/github-script@v8 - with: - script: | - github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: '-1' - }); - - const comment = `❌ Sync failed. Please check the workflow logs for details. - - [View workflow run](${context.payload.repository.html_url}/actions/runs/${context.runId})`; - - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: comment - }); - - - name: Fail workflow if push failed - if: steps.push.outputs.success == 'false' - run: exit 1