IJPL-161089 vcs marker: Initial implementation of committing separate chunk from gutter

GitOrigin-RevId: 7009ec8c785bed0074084c44495749e52c30e9e4
This commit is contained in:
Aleksandr Krasilnikov
2024-09-16 15:44:11 +02:00
committed by intellij-monorepo-bot
parent aa3a77d9a0
commit 86d58fbf8c
14 changed files with 574 additions and 38 deletions

View File

@@ -268,15 +268,14 @@ public class LineStatusMarkerPopupPanel extends JPanel {
DiffDrawUtil.LAYER_PRIORITY_LST, parentDisposable);
int currentStartOffset = currentTextRange.getStartOffset();
List<RangeHighlighter> 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<RangeHighlighter> 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();

View File

@@ -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<AnAction> = emptyList()
protected open fun createAdditionalToolbarActions(editor: Editor, range: Range, mousePosition: Point?, popupDisposable: Disposable): List<AnAction> = emptyList()
protected open fun createAdditionalInfoPanel(editor: Editor,
range: Range,

View File

@@ -83,6 +83,11 @@ public class JBPanel<T extends JBPanel> extends JPanel implements JBComponent<T>
return (T)this;
}
public final T resetPreferredHeight() {
myPreferredHeight = null;
return (T)this;
}
public final T withPreferredSize(int width, int height) {
myPreferredWidth = width;
myPreferredHeight = height;

View File

@@ -127,6 +127,7 @@ public final class VcsConfiguration implements PersistentStateComponent<VcsConfi
@XCollection(elementName = "MESSAGE")
public List<String> 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<VcsConfi
});
}
public void saveTempChunkCommitMessage(@NotNull final String comment) {
LAST_CHUNK_COMMIT_MESSAGE = comment;
}
public @NotNull String getTempChunkCommitMessage() {
return LAST_CHUNK_COMMIT_MESSAGE;
}
private static void addCommitMessage(@NotNull List<? super String> recentMessages, @NotNull String comment) {
if (recentMessages.size() >= MAX_STORED_MESSAGES) {
recentMessages.remove(0);

View File

@@ -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
activity.name.unshelve=Unshelve
# commit from gutter
commit.from.gutter.placeholder=Commit this change

View File

@@ -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);

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3176 2.42105C10.7627 1.86723 9.84952 1.85121 9.28596 2.42376L7.38018 4.31787L7.37942 4.31863L2 9.68822L2.0004 13.2222H4.8125V12.2222H3.00029L3.00005 10.1029L7.73232 5.37929L9.8791 7.52606L8.4805 8.92188H9.89612L10.9404 7.87969L12.7968 6.02031C13.3676 5.46125 13.3557 4.56317 12.8112 4.00772L11.327 2.43041L11.3176 2.42105ZM9.9978 3.1261C10.1608 2.95962 10.4362 2.95595 10.6096 3.12724L12.0867 4.69722L12.0956 4.70618C12.2663 4.87869 12.2624 5.14475 12.0962 5.30675L10.5863 6.81903L8.44084 4.67359L9.99307 3.13093L9.9978 3.1261Z" fill="#6C707E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.7129 12.3659H13.0907C13.3362 12.3659 13.5352 12.1669 13.5352 11.9214C13.5352 11.676 13.3362 11.477 13.0907 11.477H11.7129C11.507 10.4627 10.6102 9.69922 9.53516 9.69922C8.46006 9.69922 7.56328 10.4627 7.35739 11.477H5.9796C5.73414 11.477 5.53516 11.676 5.53516 11.9214C5.53516 12.1669 5.73414 12.3659 5.9796 12.3659H7.35739C7.56328 13.3802 8.46006 14.1437 9.53516 14.1437C10.6102 14.1437 11.507 13.3802 11.7129 12.3659ZM10.8685 11.9214C10.8685 12.6578 10.2715 13.2548 9.53516 13.2548C8.79878 13.2548 8.20182 12.6578 8.20182 11.9214C8.20182 11.1851 8.79878 10.5881 9.53516 10.5881C10.2715 10.5881 10.8685 11.1851 10.8685 11.9214Z" fill="#3574F0"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3176 2.42105C10.7627 1.86723 9.84952 1.85121 9.28596 2.42376L7.38018 4.31787L7.37942 4.31863L2 9.68822L2.0004 13.2222H4.8125V12.2222H3.00029L3.00005 10.1029L7.73232 5.37929L9.8791 7.52606L8.4805 8.92188H9.89612L10.9404 7.87969L12.7968 6.02031C13.3676 5.46125 13.3557 4.56317 12.8112 4.00772L11.327 2.43041L11.3176 2.42105ZM9.9978 3.1261C10.1608 2.95962 10.4362 2.95595 10.6096 3.12724L12.0867 4.69722L12.0956 4.70618C12.2663 4.87869 12.2624 5.14475 12.0962 5.30675L10.5863 6.81903L8.44084 4.67359L9.99307 3.13093L9.9978 3.1261Z" fill="#CED0D6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.7129 12.3659H13.0907C13.3362 12.3659 13.5352 12.1669 13.5352 11.9214C13.5352 11.676 13.3362 11.477 13.0907 11.477H11.7129C11.507 10.4627 10.6102 9.69922 9.53516 9.69922C8.46006 9.69922 7.56328 10.4627 7.35739 11.477H5.9796C5.73414 11.477 5.53516 11.676 5.53516 11.9214C5.53516 12.1669 5.73414 12.3659 5.9796 12.3659H7.35739C7.56328 13.3802 8.46006 14.1437 9.53516 14.1437C10.6102 14.1437 11.507 13.3802 11.7129 12.3659ZM10.8685 11.9214C10.8685 12.6578 10.2715 13.2548 9.53516 13.2548C8.79878 13.2548 8.20182 12.6578 8.20182 11.9214C8.20182 11.1851 8.79878 10.5881 9.53516 10.5881C10.2715 10.5881 10.8685 11.1851 10.8685 11.9214Z" fill="#548AF7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.5 8H3.5L2.5 14.5L14.5 8L2.5 1.5L3.192 6" stroke="#818594" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 351 B

View File

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.5 8H3.5L2.5 14.5L14.5 8L2.5 1.5L3.192 6" stroke="#818594" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 351 B

View File

@@ -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<String> {
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<AnAction> {
return createChangeListActions(editor, range, mousePosition, popupDisposable)
}
private fun createChangeListActions(editor: Editor,
range: Range,
mousePosition: Point?,
disposable: Disposable): List<AnAction> {
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<RangeState> {
internal fun collectRangeStates(): List<RangeState> {
return documentTracker.readLock {
blocks.map { RangeState(it.range, it.marker.changelistId, it.excludedFromCommit) }
}

View File

@@ -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<Change> {
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<FilePath> = emptyList()
override fun getDisplayedChanges(): List<Change> = emptyList()
override fun getDisplayedUnversionedFiles(): List<FilePath> = 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<CommitChunkWorkflow, CommitChunkPanel>() {
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<ChangelistsLocalLineStatusTracker, CommitChunkComponent>()
@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<CommitChunkService>()
}
}

View File

@@ -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
}

View File

@@ -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<NonModalCommitWorkflowHandler<*, *>>()