From abacc2530ff60cac3727e54dc5f3a0170939851a Mon Sep 17 00:00:00 2001 From: Thomas Repnik Date: Wed, 15 Jul 2026 20:44:43 +0200 Subject: [PATCH] Fix focus stolen from terminal when commit message prefix is updated panel.setCommitMessage() calls CommitMessageUi.focus() in the platform's CommitProjectPanelAdapter, yanking focus into the commit message field on every branch change (e.g. git checkout in the terminal). Set the message directly on the CommitMessage component instead (no focus request) and skip the update entirely when the computed message is unchanged. Co-Authored-By: Claude Fable 5 --- .../intellij/CommitPrefixCheckinHandler.java | 44 ++++++++-- .../CommitPrefixCheckinHandlerTest.java | 80 +++++++++++++++++++ 2 files changed, 117 insertions(+), 7 deletions(-) diff --git a/src/main/java/ch/repnik/intellij/CommitPrefixCheckinHandler.java b/src/main/java/ch/repnik/intellij/CommitPrefixCheckinHandler.java index 84cb314..22ae908 100644 --- a/src/main/java/ch/repnik/intellij/CommitPrefixCheckinHandler.java +++ b/src/main/java/ch/repnik/intellij/CommitPrefixCheckinHandler.java @@ -13,10 +13,12 @@ import com.intellij.openapi.vcs.ProjectLevelVcsManager; import com.intellij.openapi.vcs.checkin.CheckinHandler; import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl; +import com.intellij.openapi.vcs.ui.CommitMessage; import com.intellij.openapi.vcs.ui.RefreshableOnComponent; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.impl.PsiDocumentManagerImpl; import com.intellij.util.messages.MessageBusConnection; +import com.intellij.util.ui.UIUtil; import git4idea.GitLocalBranch; import git4idea.branch.GitBranchUtil; import git4idea.repo.GitRepository; @@ -53,7 +55,7 @@ private void updateCommitMessage() { PsiDocumentManager psiInstance = PsiDocumentManager.getInstance(this.panel.getProject()); if (psiInstance instanceof PsiDocumentManagerImpl) { if (!((PsiDocumentManagerImpl) psiInstance).isCommitInProgress()) { - panel.setCommitMessage(getNewCommitMessage()); + getNewCommitMessage().ifPresent(this::setCommitMessageWithoutFocus); } else { log.info("PsiDocumentManager reported commit in progress. Skipping Git Auto Prefix"); } @@ -63,6 +65,22 @@ private void updateCommitMessage() { }); } + /** + * Sets the commit message without requesting focus. + * panel.setCommitMessage() would call CommitMessageUi.focus() (see CommitProjectPanelAdapter) + * which steals the focus e.g. from the terminal when switching branches. + */ + private void setCommitMessageWithoutFocus(String newMessage) { + CommitMessage commitMessageComponent = + UIUtil.findComponentOfType(panel.getComponent(), CommitMessage.class); + if (commitMessageComponent != null) { + commitMessageComponent.setCommitMessage(newMessage); + } else { + // Fallback for unknown CheckinProjectPanel implementations (may move focus) + panel.setCommitMessage(newMessage); + } + } + @Nullable @Override public RefreshableOnComponent getBeforeCheckinConfigurationPanel() { @@ -71,18 +89,30 @@ public RefreshableOnComponent getBeforeCheckinConfigurationPanel() { return super.getBeforeCheckinConfigurationPanel(); } - private String getNewCommitMessage() { + private Optional getNewCommitMessage() { String branchName = extractBranchName(); // log.warn("BranchName: " + branchName); - Optional ticketName = getTicket(getTicketSystem(), branchName); + return calculateNewCommitMessage(getTicketSystem(), branchName, panel.getCommitMessage(), + getWrapLeft(), getWrapRight(), getIssueKeyPosition()); + } + + static Optional calculateNewCommitMessage(TicketSystem ticketSystem, String branchName, + String currentMessage, String wrapLeft, String wrapRight, Position issueKeyPosition) { + Optional ticketName = getTicket(ticketSystem, branchName); - if (ticketName.isPresent()) { - // Sets the value for the new Panel UI - return updatePrefix(ticketName.get(), panel.getCommitMessage(), getTicketSystem(), getWrapLeft(), getWrapRight(), getIssueKeyPosition()); + if (!ticketName.isPresent()) { + return Optional.empty(); + } + + String newMessage = updatePrefix(ticketName.get(), currentMessage, ticketSystem, wrapLeft, wrapRight, issueKeyPosition); + + // CommitMessage.getText() trims trailing whitespace, so compare rTrimmed to detect real changes + if (rTrim(newMessage).equals(currentMessage == null ? "" : rTrim(currentMessage))) { + return Optional.empty(); } - return panel.getCommitMessage(); + return Optional.of(newMessage); } static Optional getTicket(TicketSystem ticketSystem, String branchName) { diff --git a/src/test/java/ch/repnik/intellij/CommitPrefixCheckinHandlerTest.java b/src/test/java/ch/repnik/intellij/CommitPrefixCheckinHandlerTest.java index 41912ba..9c9ff16 100644 --- a/src/test/java/ch/repnik/intellij/CommitPrefixCheckinHandlerTest.java +++ b/src/test/java/ch/repnik/intellij/CommitPrefixCheckinHandlerTest.java @@ -504,6 +504,45 @@ public void getTicketName_branchNameStartingWithDigit_returnsTicket(TicketNameTe } + static Stream calculateNewCommitMessage_messageUnchanged_returnsEmpty() { + CalculateMessageTester startTemplate = new CalculateMessageTester().withWrapLeft("").withWrapRight(": ").withIssueKeyPosition(Position.START); + CalculateMessageTester endTemplate = new CalculateMessageTester().withWrapLeft(": ").withWrapRight("").withIssueKeyPosition(Position.END); + + return Stream.of( + Arguments.of(startTemplate.withTicketSystem(JIRA).withBranchName("main").withCurrentMessage("Some message")), + Arguments.of(startTemplate.withTicketSystem(JIRA).withBranchName("develop").withCurrentMessage("")), + Arguments.of(startTemplate.withTicketSystem(JIRA).withBranchName("feature/ABC-1234-app-not-working").withCurrentMessage("ABC-1234: This is my text")), + Arguments.of(startTemplate.withTicketSystem(JIRA).withBranchName("feature/ABC-1234-app-not-working").withCurrentMessage("ABC-1234:")), + Arguments.of(endTemplate.withTicketSystem(JIRA).withBranchName("feature/ABC-1234-app-not-working").withCurrentMessage("This is my text: ABC-1234")), + Arguments.of(startTemplate.withTicketSystem(OTHER).withBranchName("feature/5678-app-not-working").withCurrentMessage("5678: This is my text")) + ); + } + + @ParameterizedTest + @MethodSource + public void calculateNewCommitMessage_messageUnchanged_returnsEmpty(CalculateMessageTester tester) { + tester.calculate().assertEmpty(); + } + + static Stream calculateNewCommitMessage_messageChanged_returnsNewMessage() { + CalculateMessageTester startTemplate = new CalculateMessageTester().withWrapLeft("").withWrapRight(": ").withIssueKeyPosition(Position.START); + CalculateMessageTester endTemplate = new CalculateMessageTester().withWrapLeft(": ").withWrapRight("").withIssueKeyPosition(Position.END); + + return Stream.of( + Arguments.of(startTemplate.withTicketSystem(JIRA).withBranchName("feature/ABC-1234-app-not-working").withCurrentMessage(null), "ABC-1234: "), + Arguments.of(startTemplate.withTicketSystem(JIRA).withBranchName("feature/ABC-1234-app-not-working").withCurrentMessage("XYXY-837292: This is my text"), "ABC-1234: This is my text"), + Arguments.of(endTemplate.withTicketSystem(JIRA).withBranchName("feature/ABC-1234-app-not-working").withCurrentMessage("This is my text: XYXY-837292"), "This is my text: ABC-1234"), + Arguments.of(startTemplate.withTicketSystem(OTHER).withBranchName("feature/5678-app-not-working").withCurrentMessage("1111: This is my text"), "5678: This is my text") + ); + } + + @ParameterizedTest + @MethodSource + public void calculateNewCommitMessage_messageChanged_returnsNewMessage(CalculateMessageTester tester, String expectedMessage) { + tester.calculate().assertMessage(expectedMessage); + } + + @With @AllArgsConstructor @@ -572,4 +611,45 @@ void doAssertion(String expectedTicketName) { assertThat(actualTicketName.get(), is(expectedTicketName)); } } + + + @With + @AllArgsConstructor + @NoArgsConstructor + static class CalculateMessageTester { + + private TicketSystem ticketSystem; + private String branchName; + private String currentMessage; + private String wrapLeft; + private String wrapRight; + private Position issueKeyPosition; + + CalculateMessageAsserter calculate() { + Optional newMessage = + CommitPrefixCheckinHandler.calculateNewCommitMessage( + ticketSystem, branchName, currentMessage, wrapLeft, wrapRight, issueKeyPosition); + return new CalculateMessageAsserter(newMessage); + } + + @Override + public String toString() { + return String.format("%s, %s, %s", ticketSystem.toString(), branchName, currentMessage); + } + } + + @RequiredArgsConstructor + static class CalculateMessageAsserter { + + private final Optional actualMessage; + + void assertEmpty() { + assertThat(actualMessage.isPresent(), is(false)); + } + + void assertMessage(String expectedMessage) { + assertThat(actualMessage.isPresent(), is(true)); + assertThat(actualMessage.get(), is(expectedMessage)); + } + } }