diff --git a/platform/diff-impl/src/com/intellij/openapi/vcs/ex/LineStatusMarkerPopupPanel.java b/platform/diff-impl/src/com/intellij/openapi/vcs/ex/LineStatusMarkerPopupPanel.java index bbb8856b50f0..d29138b70658 100644 --- a/platform/diff-impl/src/com/intellij/openapi/vcs/ex/LineStatusMarkerPopupPanel.java +++ b/platform/diff-impl/src/com/intellij/openapi/vcs/ex/LineStatusMarkerPopupPanel.java @@ -268,15 +268,14 @@ public class LineStatusMarkerPopupPanel extends JPanel { DiffDrawUtil.LAYER_PRIORITY_LST, parentDisposable); int currentStartOffset = currentTextRange.getStartOffset(); - List highlighters = new ArrayList<>(); - highlighters.addAll( - new DiffDrawUtil.LineHighlighterBuilder(editor, startLine, endLine, TextDiffType.MODIFIED) - .withLayerPriority(DiffDrawUtil.LAYER_PRIORITY_LST) - .withIgnored(true) - .withHideStripeMarkers(true) - .withHideGutterMarkers(true) - .done()); + List highlighters = + new ArrayList<>(new DiffDrawUtil.LineHighlighterBuilder(editor, startLine, endLine, TextDiffType.MODIFIED) + .withLayerPriority(DiffDrawUtil.LAYER_PRIORITY_LST) + .withIgnored(true) + .withHideStripeMarkers(true) + .withHideGutterMarkers(true) + .done()); for (DiffFragment fragment : wordDiff) { int currentStart = currentStartOffset + fragment.getStartOffset2(); diff --git a/platform/diff-impl/src/com/intellij/openapi/vcs/ex/LineStatusTrackerMarkerRenderer.kt b/platform/diff-impl/src/com/intellij/openapi/vcs/ex/LineStatusTrackerMarkerRenderer.kt index 917167b2bf40..904c2a6924c5 100644 --- a/platform/diff-impl/src/com/intellij/openapi/vcs/ex/LineStatusTrackerMarkerRenderer.kt +++ b/platform/diff-impl/src/com/intellij/openapi/vcs/ex/LineStatusTrackerMarkerRenderer.kt @@ -29,14 +29,15 @@ abstract class LineStatusTrackerMarkerRenderer( final override fun createPopupPanel(editor: Editor, range: Range, mousePosition: Point?, - disposable: Disposable): LineStatusMarkerPopupPanel { + popupDisposable: Disposable): LineStatusMarkerPopupPanel { var editorComponent: JComponent? = null if (range.hasVcsLines()) { - editorComponent = createVcsContentComponent(range, editor, disposable) + editorComponent = createVcsContentComponent(range, editor, popupDisposable) } - val actions = createToolbarActions(editor, range, mousePosition) - val toolbar = LineStatusMarkerPopupPanel.buildToolbar(editor, actions, disposable) - val additionalInfoPanel = createAdditionalInfoPanel(editor, range, mousePosition, disposable) + val actions = createToolbarActions(editor, range, mousePosition) + + createAdditionalToolbarActions(editor, range, mousePosition, popupDisposable) + val toolbar = LineStatusMarkerPopupPanel.buildToolbar(editor, actions, popupDisposable) + val additionalInfoPanel = createAdditionalInfoPanel(editor, range, mousePosition, popupDisposable) return LineStatusMarkerPopupPanel.create(editor, toolbar, editorComponent, additionalInfoPanel) } @@ -51,6 +52,7 @@ abstract class LineStatusTrackerMarkerRenderer( } protected open fun createToolbarActions(editor: Editor, range: Range, mousePosition: Point?): List = emptyList() + protected open fun createAdditionalToolbarActions(editor: Editor, range: Range, mousePosition: Point?, popupDisposable: Disposable): List = emptyList() protected open fun createAdditionalInfoPanel(editor: Editor, range: Range, diff --git a/platform/util/ui/src/com/intellij/ui/components/JBPanel.java b/platform/util/ui/src/com/intellij/ui/components/JBPanel.java index fcb850a31854..58cbd6336cad 100644 --- a/platform/util/ui/src/com/intellij/ui/components/JBPanel.java +++ b/platform/util/ui/src/com/intellij/ui/components/JBPanel.java @@ -83,6 +83,11 @@ public class JBPanel extends JPanel implements JBComponent return (T)this; } + public final T resetPreferredHeight() { + myPreferredHeight = null; + return (T)this; + } + public final T withPreferredSize(int width, int height) { myPreferredWidth = width; myPreferredHeight = height; diff --git a/platform/vcs-api/src/com/intellij/openapi/vcs/VcsConfiguration.java b/platform/vcs-api/src/com/intellij/openapi/vcs/VcsConfiguration.java index 154266b1e5f3..5303633816af 100644 --- a/platform/vcs-api/src/com/intellij/openapi/vcs/VcsConfiguration.java +++ b/platform/vcs-api/src/com/intellij/openapi/vcs/VcsConfiguration.java @@ -127,6 +127,7 @@ public final class VcsConfiguration implements PersistentStateComponent myLastCommitMessages = new ArrayList<>(); public @Nullable String LAST_COMMIT_MESSAGE = null; + public @NotNull String LAST_CHUNK_COMMIT_MESSAGE = ""; public boolean MAKE_NEW_CHANGELIST_ACTIVE = false; public boolean PRESELECT_EXISTING_CHANGELIST = false; @@ -171,6 +172,14 @@ public final class VcsConfiguration implements PersistentStateComponent recentMessages, @NotNull String comment) { if (recentMessages.size() >= MAX_STORED_MESSAGES) { recentMessages.remove(0); diff --git a/platform/vcs-api/vcs-api-core/resources/messages/VcsBundle.properties b/platform/vcs-api/vcs-api-core/resources/messages/VcsBundle.properties index 3a6fa1bc2ea7..db6fceef43bf 100644 --- a/platform/vcs-api/vcs-api-core/resources/messages/VcsBundle.properties +++ b/platform/vcs-api/vcs-api-core/resources/messages/VcsBundle.properties @@ -1268,4 +1268,7 @@ activity.name.get.from=Get from {0} activity.name.shelve=Shelve changes activity.name.rollback=Rollback activity.name.apply.patch=Apply patch -activity.name.unshelve=Unshelve \ No newline at end of file +activity.name.unshelve=Unshelve + +# commit from gutter +commit.from.gutter.placeholder=Commit this change \ No newline at end of file diff --git a/platform/vcs-impl/gen/com/intellij/platform/vcs/impl/icons/PlatformVcsImplIcons.java b/platform/vcs-impl/gen/com/intellij/platform/vcs/impl/icons/PlatformVcsImplIcons.java index d3ef944ed1a5..4d38cf42614f 100644 --- a/platform/vcs-impl/gen/com/intellij/platform/vcs/impl/icons/PlatformVcsImplIcons.java +++ b/platform/vcs-impl/gen/com/intellij/platform/vcs/impl/icons/PlatformVcsImplIcons.java @@ -18,6 +18,8 @@ public final class PlatformVcsImplIcons { private static @NotNull Icon load(@NotNull String expUIPath, @NotNull String path, int cacheKey, int flags) { return IconManager.getInstance().loadRasterizedIcon(path, expUIPath, PlatformVcsImplIcons.class.getClassLoader(), cacheKey, flags); } + /** 16x16 */ public static final @NotNull Icon AmendInline = load("icons/AmendInline.svg", 1594676608, 2); + /** 16x16 */ public static final @NotNull Icon CommitInline = load("icons/CommitInline.svg", 747759737, 2); /** 16x16 */ public static final @NotNull Icon Shelve = load("icons/new/stash.svg", "icons/Shelve.svg", -1645293825, 2); /** 16x16 */ public static final @NotNull Icon Stash = load("icons/new/stash.svg", "icons/Stash.svg", -451629034, 2); /** 16x16 */ public static final @NotNull Icon Vcs = load("icons/new/vcs.svg", 1023462254, 2); diff --git a/platform/vcs-impl/resources/icons/AmendInline.svg b/platform/vcs-impl/resources/icons/AmendInline.svg new file mode 100644 index 000000000000..e06c571b75ab --- /dev/null +++ b/platform/vcs-impl/resources/icons/AmendInline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/platform/vcs-impl/resources/icons/AmendInline_dark.svg b/platform/vcs-impl/resources/icons/AmendInline_dark.svg new file mode 100644 index 000000000000..bc0d32cc1bc6 --- /dev/null +++ b/platform/vcs-impl/resources/icons/AmendInline_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/platform/vcs-impl/resources/icons/CommitInline.svg b/platform/vcs-impl/resources/icons/CommitInline.svg new file mode 100644 index 000000000000..198e04e6dfa1 --- /dev/null +++ b/platform/vcs-impl/resources/icons/CommitInline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/platform/vcs-impl/resources/icons/CommitInline_dark.svg b/platform/vcs-impl/resources/icons/CommitInline_dark.svg new file mode 100644 index 000000000000..198e04e6dfa1 --- /dev/null +++ b/platform/vcs-impl/resources/icons/CommitInline_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/PartialLocalLineStatusTracker.kt b/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/PartialLocalLineStatusTracker.kt index 7e7bf91cf756..d10ef1d73712 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/PartialLocalLineStatusTracker.kt +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/PartialLocalLineStatusTracker.kt @@ -4,12 +4,10 @@ package com.intellij.openapi.vcs.ex import com.intellij.codeWithMe.ClientId import com.intellij.diff.util.DiffUtil import com.intellij.diff.util.Side +import com.intellij.icons.AllIcons import com.intellij.ide.DataManager import com.intellij.openapi.Disposable -import com.intellij.openapi.actionSystem.ActionManager -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.DefaultActionGroup -import com.intellij.openapi.actionSystem.Separator +import com.intellij.openapi.actionSystem.* import com.intellij.openapi.command.CommandEvent import com.intellij.openapi.command.CommandListener import com.intellij.openapi.command.CommandProcessor @@ -25,6 +23,7 @@ import com.intellij.openapi.editor.event.DocumentEvent import com.intellij.openapi.editor.event.DocumentListener import com.intellij.openapi.keymap.KeymapUtil import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.DumbAwareToggleAction import com.intellij.openapi.project.Project import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.util.Disposer @@ -37,6 +36,7 @@ import com.intellij.openapi.vcs.changes.ChangeListWorker import com.intellij.openapi.vcs.changes.LocalChangeList import com.intellij.openapi.vcs.ex.DocumentTracker.Block import com.intellij.openapi.vcs.ex.LineStatusTrackerBlockOperations.Companion.isSelectedByLine +import com.intellij.openapi.vcs.ex.commit.CommitChunkService import com.intellij.openapi.vcs.impl.ActiveChangeListTracker import com.intellij.openapi.vcs.impl.LineStatusTrackerManager import com.intellij.openapi.vfs.VirtualFile @@ -198,7 +198,7 @@ class ChangelistsLocalLineStatusTracker internal constructor(project: Project, override fun getAffectedChangeListsIds(): List { return documentTracker.readLock { - assert(!affectedChangeLists.isEmpty()) + assert(!affectedChangeLists.isEmpty) affectedChangeLists.toList() } } @@ -654,33 +654,47 @@ class ChangelistsLocalLineStatusTracker internal constructor(project: Project, mousePosition: Point?, disposable: Disposable): JComponent? { val superPanel = super.createAdditionalInfoPanel(editor, range, mousePosition, disposable) - val changelistPanel = createChangelistInfoPanel(editor, range, mousePosition, disposable) - if (superPanel == null || changelistPanel == null) return superPanel ?: changelistPanel + val commitPanel = createCommitPanel(editor, range, mousePosition, disposable) - return JBUI.Panels.simplePanel(changelistPanel) + if (superPanel == null || commitPanel == null) return superPanel ?: commitPanel + + val panel = JBUI.Panels.simplePanel(commitPanel) .addToRight(superPanel) .andTransparent() + return panel } - private fun createChangelistInfoPanel(editor: Editor, - range: Range, - mousePosition: Point?, - disposable: Disposable): JComponent? { + private fun createCommitPanel(editor: Editor, range: Range, point: Point?, disposable: Disposable): JComponent? { if (range !is LocalRange) return null + return CommitChunkService.getInstance(project!!).getComponent(tracker, range, disposable).getCommitInput() + } + + override fun createAdditionalToolbarActions(editor: Editor, range: Range, mousePosition: Point?, popupDisposable: Disposable): List { + return createChangeListActions(editor, range, mousePosition, popupDisposable) + } + + private fun createChangeListActions(editor: Editor, + range: Range, + mousePosition: Point?, + disposable: Disposable): List { + if (range !is LocalRange) return emptyList() val changeLists = ChangeListManager.getInstance(tracker.project).changeLists - val rangeList = changeLists.find { it.id == range.changelistId } ?: return null + val rangeList = changeLists.find { it.id == range.changelistId } ?: return emptyList() - val group = DefaultActionGroup() + val group = DefaultActionGroup(VcsBundle.message("ex.changelists"), null, AllIcons.Vcs.Changelist) + group.isPopup = true if (changeLists.size > 1) { group.add(Separator(VcsBundle.message("ex.changelists"))) for (changeList in changeLists) { - group.add(MoveToChangeListAction(editor, range, mousePosition, changeList)) + group.add(MoveToChangeListToggleAction(editor, range, mousePosition, changeList)) } group.add(Separator.getInstance()) } group.add(MoveToAnotherChangeListAction(editor, range, mousePosition)) + return listOf(group) + val link = DropDownLink(rangeList.name) { linkLabel -> val dataContext = DataManager.getInstance().getDataContext(linkLabel) @@ -702,7 +716,7 @@ class ChangelistsLocalLineStatusTracker internal constructor(project: Project, link.toolTipText = VcsBundle.message("ex.move.lines.to.another.changelist.0", KeymapUtil.getShortcutText(shortcuts.first())) } - return link + //return link } private inner class MoveToAnotherChangeListAction(editor: Editor, range: Range, val mousePosition: Point?) @@ -719,17 +733,31 @@ class ChangelistsLocalLineStatusTracker internal constructor(project: Project, } } - private inner class MoveToChangeListAction(editor: Editor, range: Range, val mousePosition: Point?, val changelist: LocalChangeList) - : LineStatusMarkerPopupActions.RangeMarkerAction(editor, tracker, range, null) { + private inner class MoveToChangeListToggleAction( + private val editor: Editor, private val range: Range, val mousePosition: Point?, val changelist: LocalChangeList + ) : DumbAwareToggleAction(changelist.name) { init { templatePresentation.setText(StringUtil.trimMiddle(changelist.name, 60), false) + templatePresentation.keepPopupOnPerform = KeepPopupOnPerform.Never } - override fun isEnabled(editor: Editor, range: Range): Boolean = range is LocalRange + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT - override fun actionPerformed(editor: Editor, range: Range) { - tracker.moveToChangelist(range, changelist) - reopenRange(editor, range, mousePosition) + override fun update(e: AnActionEvent) { + super.update(e) + val newRange = rangesSource.findRange(range) + e.presentation.setEnabled(newRange != null && !editor.isDisposed() && newRange is LocalRange) + } + + override fun isSelected(e: AnActionEvent): Boolean { + val newRange = rangesSource.findRange(range) + return (newRange as LocalRange).changelistId == changelist.id + } + + override fun setSelected(e: AnActionEvent, state: Boolean) { + val newRange = rangesSource.findRange(range) ?: return + tracker.moveToChangelist(newRange, changelist) + reopenRange(editor, newRange, mousePosition) } } @@ -816,6 +844,13 @@ class ChangelistsLocalLineStatusTracker internal constructor(project: Project, fireExcludedFromCommitChanged() } + fun excludeAllBlocks() { + documentTracker.writeLock { + blocks.forEach { b -> b.excludedFromCommit = RangeExclusionState.Excluded } + } + fireExcludedFromCommitChanged() + } + override fun setPartiallyExcludedFromCommit(lines: BitSet, side: Side, isExcluded: Boolean) { documentTracker.writeLock { for (block in blocks) { @@ -903,7 +938,7 @@ class ChangelistsLocalLineStatusTracker internal constructor(project: Project, } @RequiresReadLock - private fun collectRangeStates(): List { + internal fun collectRangeStates(): List { return documentTracker.readLock { blocks.map { RangeState(it.range, it.marker.changelistId, it.excludedFromCommit) } } diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/commit/CommitChunkComponent.kt b/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/commit/CommitChunkComponent.kt new file mode 100644 index 000000000000..7af19bd5f34b --- /dev/null +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/commit/CommitChunkComponent.kt @@ -0,0 +1,399 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.openapi.vcs.ex.commit + +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.* +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vcs.CheckinProjectPanel +import com.intellij.openapi.vcs.FilePath +import com.intellij.openapi.vcs.ProjectLevelVcsManager +import com.intellij.openapi.vcs.VcsBundle +import com.intellij.openapi.vcs.changes.Change +import com.intellij.openapi.vcs.changes.ChangeListManager +import com.intellij.openapi.vcs.changes.CommitExecutor +import com.intellij.openapi.vcs.ex.ChangelistsLocalLineStatusTracker +import com.intellij.openapi.vcs.ex.LocalRange +import com.intellij.openapi.vcs.ex.RangeExclusionState +import com.intellij.openapi.vcs.ui.CommitMessage +import com.intellij.platform.vcs.impl.icons.PlatformVcsImplIcons +import com.intellij.ui.components.panels.Wrapper +import com.intellij.util.EventDispatcher +import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.ui.Animator +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.components.BorderLayoutPanel +import com.intellij.vcs.commit.* +import java.awt.Color +import java.awt.Dimension +import java.awt.event.FocusAdapter +import java.awt.event.FocusEvent +import javax.swing.JComponent + +private class CommitChunkPanel(private val tracker: ChangelistsLocalLineStatusTracker, + private val amendCommitHandler: NonModalAmendCommitHandler) : NonModalCommitPanel(tracker.project) { + override val commitProgressUi: CommitProgressUi = object : CommitProgressPanel() { + override var isEmptyMessage: Boolean + get() = commitMessage.text.isBlank() + set(_) {} + } + + private val executorEventDispatcher = EventDispatcher.create(CommitExecutorListener::class.java) + + private val rightWrapper = Wrapper() + private val bottomWrapper = Wrapper() + + override var editedCommit: EditedCommitPresentation? = null + + private val commitAction = object : DumbAwareAction(VcsBundle.message("commit.from.gutter.placeholder"), null, PlatformVcsImplIcons.CommitInline) { + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = commitMessage.text.isNotBlank() + } + + override fun actionPerformed(e: AnActionEvent) { + executorEventDispatcher.multicaster.executorCalled(null) + } + } + + private val amendCommitToggle = object : ToggleAction(VcsBundle.message("checkbox.amend") , null, PlatformVcsImplIcons.AmendInline) { + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT + + override fun update(e: AnActionEvent) { + super.update(e) + val p = e.presentation + p.isVisible = amendCommitHandler.isAmendCommitModeSupported() == true + p.isEnabled = isVisible && amendCommitHandler.isAmendCommitModeTogglingEnabled == true + } + + override fun isSelected(e: AnActionEvent): Boolean = amendCommitHandler.isAmendCommitMode + + override fun setSelected(e: AnActionEvent, state: Boolean) { + amendCommitHandler.isAmendCommitMode = state + } + }.apply { + val amendShortcut = ActionManager.getInstance().getAction("Vcs.ToggleAmendCommitMode").shortcutSet + registerCustomShortcutSet(amendShortcut, this@CommitChunkPanel, this@CommitChunkPanel) + } + + var forcedWidth: Int = Spec.DEFAULT_WIDTH + val actionToolbar: ActionToolbar + + init { + actionToolbar = buildActions() + + // layout + rightWrapper.setContent(actionToolbar.component) + + centerPanel.removeAll() + centerPanel + .addToCenter(commitMessage) + .addToRight(rightWrapper) + .addToBottom(BorderLayoutPanel().addToRight(bottomWrapper).andTransparent()) + + // ui adjustment + centerPanel.andTransparent().withBackground(Spec.INPUT_BACKGROUND) + resetPreferredHeight() + andTransparent() + + val editor = commitMessage.editorField.getEditor(true) + commitMessage.editorField.setPlaceholder(VcsBundle.message("commit.from.gutter.placeholder")) + if (editor != null) { + adjustEditorSettings(editor) + centerPanel.border = CommitInputBorder(editor, this) + } + + setupResizing(commitMessage) + setupDocumentLengthTracker(commitMessage) + } + + private fun setupDocumentLengthTracker(message: CommitMessage) { + message.editorField.addDocumentListener(object : DocumentListener { + override fun documentChanged(event: DocumentEvent) { + val length = event.document.textLength + val lineCount = event.document.lineCount + process(length, lineCount) + } + + private fun process(length: Int, lineCount: Int) { + if (length > Spec.INLINED_ACTIONS_TEXT_LIMIT || lineCount > 1) { + rightWrapper.setContent(null) + bottomWrapper.setContent(actionToolbar.component) + } else { + bottomWrapper.setContent(null) + rightWrapper.setContent(actionToolbar.component) + } + } + }) + } + + private fun buildActions(): ActionToolbar { + val actionGroup = DefaultActionGroup(listOf(amendCommitToggle, Separator.create(), commitAction)) + val toolbar = ActionManager.getInstance().createActionToolbar("CommitChange", actionGroup, true) + .apply { + minimumButtonSize = Spec.MINIMUM_BUTTON_SIZE + } + + return toolbar.apply { + targetComponent = commitMessage + component.border = JBUI.Borders.empty() + component.isOpaque = false + } + } + + override fun addExecutorListener(listener: CommitExecutorListener, parent: Disposable) { + executorEventDispatcher.addListener(listener, parent) + } + + + override fun getIncludedChanges(): List { + val rangeStates = tracker.collectRangeStates() + val rangeToCommit = rangeStates.first { it.excludedFromCommit == RangeExclusionState.Included } + val changelistId = rangeToCommit.changelistId + val changeList = ChangeListManager.getInstance(project).getChangeList(changelistId)!! + + val changes = changeList.changes.filter { it.virtualFile == tracker.virtualFile } + return changes + } + + private val animator = lazy { + object : Animator("Commit Input Animation", Spec.Animation.FRAMES, Spec.Animation.DURATION, false, true) { + override fun paintNow(frame: Int, totalFrames: Int, cycle: Int) { + val nextWidth = (Spec.MAX_WIDTH - Spec.DEFAULT_WIDTH) / totalFrames * frame + Spec.DEFAULT_WIDTH + this@CommitChunkPanel.resizeInput(nextWidth) + } + } + } + + private fun setupResizing(commitMessage: CommitMessage) { + commitMessage.editorField.addFocusListener(object : FocusAdapter() { + override fun focusGained(e: FocusEvent?) { + animator.value.resume() + } + }) + } + + private fun resizeInput(newWidth: Int) { + forcedWidth = newWidth + revalidate() + repaint() + } + + + override fun getPreferredSize(): Dimension { + val pref = super.preferredSize + pref.height = minOf(pref.height, Spec.MAX_HEIGHT) + pref.width = forcedWidth + return pref + } + + override fun activate(): Boolean = true + override fun getIncludedUnversionedFiles(): List = emptyList() + override fun getDisplayedChanges(): List = emptyList() + override fun getDisplayedUnversionedFiles(): List = emptyList() + + fun resetSize() { + resizeInput(Spec.DEFAULT_WIDTH) + animator.value.reset() + } +} + +private class CommitChunkWorkflow(project: Project) : NonModalCommitWorkflow(project) { + lateinit var state: ChangeListCommitState + + init { + val vcses = ProjectLevelVcsManager.getInstance(project).allActiveVcss.toSet() + updateVcses(vcses) + } + + override val isDefaultCommitEnabled: Boolean + get() = true + + override fun performCommit(sessionInfo: CommitSessionInfo) { + val committer = LocalChangesCommitter(project, state, commitContext) + addCommonResultHandlers(sessionInfo, committer) + committer.addResultHandler(ShowNotificationCommitResultHandler(committer)) + committer.runCommit(VcsBundle.message("commit.changes"), false) + } +} + +private class CommitChunkWorkFlowHandler( + val tracker: ChangelistsLocalLineStatusTracker, + val rangeProvider: () -> LocalRange, +) : NonModalCommitWorkflowHandler() { + override val workflow: CommitChunkWorkflow = CommitChunkWorkflow(tracker.project) + override val amendCommitHandler: NonModalAmendCommitHandler = NonModalAmendCommitHandler(this) + override val ui: CommitChunkPanel = CommitChunkPanel(tracker, amendCommitHandler) + override val commitPanel: CheckinProjectPanel = CommitProjectPanelAdapter(this) + + private val commitMessagePolicy = ChunkCommitMessagePolicy(project, ui.commitMessageUi) + + init { + ui.addExecutorListener(this, this) + workflow.addListener(this, this) + workflow.addVcsCommitListener(object : CommitStateCleaner() { + override fun onSuccess() { + commitMessagePolicy.onAfterCommit() + super.onSuccess() + } + }, this) + workflow.addVcsCommitListener(PostCommitChecksRunner(), this) + commitMessagePolicy.init() + + setupDumbModeTracking() + setupCommitHandlersTracking() + setupCommitChecksResultTracking() + vcsesChanged() + } + + override suspend fun updateWorkflow(sessionInfo: CommitSessionInfo): Boolean { + workflow.state = getCommitState() + return true + } + + private fun getCommitState(): ChangeListCommitState { + val changes = getIncludedChanges() + val rangeStates = tracker.collectRangeStates() + val first = rangeStates.first { it.excludedFromCommit == RangeExclusionState.Included } + val changeList = ChangeListManager.getInstance(project).getChangeList(first.changelistId)!! + return ChangeListCommitState(changeList, changes, ui.commitMessage.text) + } + + override fun executorCalled(executor: CommitExecutor?) { + tracker.excludeAllBlocks() + tracker.setExcludedFromCommit(rangeProvider(), false) + super.executorCalled(executor) + } + + override fun saveCommitMessageBeforeCommit() { + commitMessagePolicy.onBeforeCommit() + } + + fun setPopup(popupDisposable: Disposable) { + ui.resetSize() + + workflow.addListener(object : CommitWorkflowListener { + override fun executionStarted() { + Disposer.dispose(popupDisposable) + } + }, popupDisposable) + + Disposer.register(popupDisposable, Disposable { + commitMessagePolicy.saveTempChunkCommitMessage(ui.commitMessageUi.text) + }) + + commitMessagePolicy.init() + } +} + +internal class CommitChunkComponent( + tracker: ChangelistsLocalLineStatusTracker, +) { + internal var range: LocalRange? = null + + private val workflowHandler = CommitChunkWorkFlowHandler(tracker) { range!! } + + init { + Disposer.register(tracker.disposable, workflowHandler) + } + + fun getCommitInput(): JComponent = workflowHandler.ui + + fun setPopup(disposable: Disposable) { + workflowHandler.setPopup(disposable) + } +} + +private fun adjustEditorSettings(editor: EditorEx) { + editor.scrollPane.border = JBUI.Borders.empty() + editor.backgroundColor = Spec.INPUT_BACKGROUND + editor.settings.isShowIntentionBulb = false +} + +private object Spec { + val DEFAULT_WIDTH: Int + get() = JBUI.scale(255) + + val MAX_WIDTH: Int + get() = JBUI.scale(660) + + val MAX_HEIGHT: Int + get() = JBUI.scale(100) + + val INPUT_BACKGROUND: Color + get() = EditorColorsManager.getInstance().globalScheme.defaultBackground + + // actions will be moved to the bottom after a message reaches this limit + const val INLINED_ACTIONS_TEXT_LIMIT: Int = 50 + + val MINIMUM_BUTTON_SIZE: Dimension = Dimension(22, 22) + + object Animation { + const val FRAMES = 30 + const val DURATION = 200 + } +} + +private class ChunkCommitMessagePolicy( + project: Project, + private val commitMessageUi: CommitMessageUi, +) : AbstractCommitMessagePolicy(project) { + + fun init() { + commitMessageUi.text = getCommitMessage() + } + + fun onBeforeCommit() { + val commitMessage = commitMessageUi.text + vcsConfiguration.saveCommitMessage(commitMessage) + } + + fun onAfterCommit() { + saveTempChunkCommitMessage("") + commitMessageUi.text = getCommitMessage() + } + + private fun getCommitMessage(): String { + return vcsConfiguration.tempChunkCommitMessage + } + + fun saveTempChunkCommitMessage(commitMessage: String) { + vcsConfiguration.saveTempChunkCommitMessage(commitMessage) + } +} + +@Service(Service.Level.PROJECT) +internal class CommitChunkService() { + private val components = mutableMapOf() + + @RequiresEdt + fun getComponent(tracker: ChangelistsLocalLineStatusTracker, range: LocalRange, popupDisposable: Disposable): CommitChunkComponent { + return components.getOrPut(tracker) { + createComponentForTracker(tracker) + }.apply { + this.range = range + this.setPopup(popupDisposable) + } + } + + private fun createComponentForTracker(tracker: ChangelistsLocalLineStatusTracker): CommitChunkComponent { + val component = CommitChunkComponent(tracker) + Disposer.register(tracker.disposable, Disposable { + components.remove(tracker) + }) + return component + } + + companion object { + fun getInstance(project: Project) = project.service() + } +} + diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/commit/CommitInputBorder.kt b/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/commit/CommitInputBorder.kt new file mode 100644 index 000000000000..bde49c119fe0 --- /dev/null +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/commit/CommitInputBorder.kt @@ -0,0 +1,67 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.openapi.vcs.ex.commit + +import com.intellij.ide.ui.laf.darcula.DarculaUIUtil +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.editor.ex.FocusChangeListener +import com.intellij.openapi.ui.ErrorBorderCapable +import com.intellij.util.ui.JBInsets +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.MacUIUtil +import java.awt.* +import java.awt.geom.Rectangle2D +import javax.swing.JComponent +import javax.swing.border.Border + +internal class CommitInputBorder( + private val editor: EditorEx, + private val borderOwner: JComponent, +) : Border, ErrorBorderCapable { + init { + editor.addFocusListener(object : FocusChangeListener { + override fun focusGained(editor: Editor) = repaintOwner() + override fun focusLost(editor: Editor) = repaintOwner() + private fun repaintOwner() { + borderOwner.repaint() + } + }) + } + + override fun paintBorder(c: Component, g: Graphics, x: Int, y: Int, width: Int, height: Int) { + val hasFocus = editor.contentComponent.hasFocus() + val r = Rectangle(x, y, width, height) + val g2 = g.create() as Graphics2D + try { + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, + if (MacUIUtil.USE_QUARTZ) RenderingHints.VALUE_STROKE_PURE else RenderingHints.VALUE_STROKE_NORMALIZE) + JBInsets.removeFrom(r, JBUI.insets(1)) + g2.translate(r.x, r.y) + val bw = DarculaUIUtil.BW.float + val outer = Rectangle2D.Float(bw, bw, r.width - bw * 2, r.height - bw * 2) + g2.color = c.background + g2.fill(outer) + + if (editor.contentComponent.isEnabled && editor.contentComponent.isVisible) { + val op = DarculaUIUtil.getOutline(c as JComponent) + val hasFocusInPopup = hasFocus //&& !AIChatPopup.chatInPopupEnabled(parent.mode) + if (op == null) { + g2.color = if (hasFocusInPopup) JBUI.CurrentTheme.Focus.focusColor() + else DarculaUIUtil.getOutlineColor(editor.contentComponent.isEnabled, false) + } + else { + op.setGraphicsColor(g2, hasFocus) + } + DarculaUIUtil.doPaint(g2, r.width, r.height, 5f, if (hasFocus) 1f else 0.5f, true) + } + } + finally { + g2.dispose() + } + } + + override fun getBorderInsets(c: Component): Insets = JBInsets.create(4, 8).asUIResource() + override fun isBorderOpaque(): Boolean = true +} + diff --git a/platform/vcs-impl/src/com/intellij/vcs/commit/NonModalCommitWorkflowHandler.kt b/platform/vcs-impl/src/com/intellij/vcs/commit/NonModalCommitWorkflowHandler.kt index cb168108f12f..55af77a3910d 100644 --- a/platform/vcs-impl/src/com/intellij/vcs/commit/NonModalCommitWorkflowHandler.kt +++ b/platform/vcs-impl/src/com/intellij/vcs/commit/NonModalCommitWorkflowHandler.kt @@ -49,7 +49,6 @@ import com.intellij.vcs.commit.AbstractCommitWorkflow.Companion.getCommitExecuto import kotlinx.coroutines.* import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.Nls -import java.lang.Runnable import kotlin.properties.Delegates.observable private val LOG = logger>()