diff --git a/plugins/terminal/resources/META-INF/plugin.xml b/plugins/terminal/resources/META-INF/plugin.xml index 6915fdb9a21d..6268421ed104 100644 --- a/plugins/terminal/resources/META-INF/plugin.xml +++ b/plugins/terminal/resources/META-INF/plugin.xml @@ -30,6 +30,9 @@ + + + diff --git a/plugins/terminal/resources/META-INF/terminal.xml b/plugins/terminal/resources/META-INF/terminal.xml index 94c46b9d4766..a601810c7a58 100644 --- a/plugins/terminal/resources/META-INF/terminal.xml +++ b/plugins/terminal/resources/META-INF/terminal.xml @@ -11,6 +11,8 @@ key="configurable.TerminalOptionsConfigurable.display.name" id="terminal" instance="org.jetbrains.plugins.terminal.TerminalOptionsConfigurable"/> + + diff --git a/plugins/terminal/resources/messages/TerminalBundle.properties b/plugins/terminal/resources/messages/TerminalBundle.properties index a991ee121bd8..c5ec49992793 100644 --- a/plugins/terminal/resources/messages/TerminalBundle.properties +++ b/plugins/terminal/resources/messages/TerminalBundle.properties @@ -4,6 +4,7 @@ action.Terminal.CloseTab.text=Close Tab action.Terminal.SwitchFocusToEditor.text=Switch Focus To Editor action.Terminal.MoveToolWindowTabRight.text=Move Tab Right action.Terminal.MoveToolWindowTabLeft.text=Move Tab Left +action.Terminal.MoveToEditor.text=Move to Editor action.Terminal.RenameSession.text=Rename Session action.Terminal.OpenInTerminal.text=Open in Terminal action.Terminal.OpenInTerminal.RevealInPopup.text=Terminal diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/TerminalToolWindowManager.java b/plugins/terminal/src/org/jetbrains/plugins/terminal/TerminalToolWindowManager.java index 562b468b08a9..bffc704855df 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/TerminalToolWindowManager.java +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/TerminalToolWindowManager.java @@ -41,9 +41,14 @@ import com.intellij.terminal.TerminalTitleListener; import com.intellij.terminal.ui.TerminalWidget; import com.intellij.terminal.ui.TerminalWidgetKt; import com.intellij.toolWindow.InternalDecoratorImpl; +import com.intellij.ui.awt.RelativePoint; +import com.intellij.ui.awt.RelativeRectangle; import com.intellij.ui.content.Content; import com.intellij.ui.content.ContentFactory; import com.intellij.ui.content.ContentManager; +import com.intellij.ui.docking.DockContainer; +import com.intellij.ui.docking.DockManager; +import com.intellij.ui.docking.DockableContent; import com.intellij.util.ArrayUtil; import com.intellij.util.ObjectUtils; import com.intellij.util.PathUtil; @@ -62,6 +67,7 @@ import org.jetbrains.plugins.terminal.arrangement.TerminalCommandHistoryManager; import org.jetbrains.plugins.terminal.arrangement.TerminalWorkingDirectoryManager; import org.jetbrains.plugins.terminal.block.BlockTerminalPromotionService; import org.jetbrains.plugins.terminal.ui.TerminalContainer; +import org.jetbrains.plugins.terminal.vfs.TerminalSessionVirtualFileImpl; import javax.swing.*; import java.awt.event.ActionEvent; @@ -82,6 +88,7 @@ public final class TerminalToolWindowManager implements Disposable { private ToolWindow myToolWindow; private final Project myProject; private final AbstractTerminalRunner myTerminalRunner; + private TerminalDockContainer myDockContainer; private final Map myContainerByWidgetMap = new HashMap<>(); @NotNull @@ -151,6 +158,11 @@ public final class TerminalToolWindowManager implements Disposable { } } }); + + if (myDockContainer == null) { + myDockContainer = new TerminalDockContainer(); + DockManager.getInstance(myProject).register(myDockContainer, toolWindow.getDisposable()); + } } void restoreTabs(@Nullable TerminalArrangementState arrangementState) { @@ -173,7 +185,7 @@ public final class TerminalToolWindowManager implements Disposable { } public void createNewSession(@NotNull AbstractTerminalRunner terminalRunner, @Nullable TerminalTabState tabState) { - createNewSession(terminalRunner, tabState, true, true); + createNewSession(terminalRunner, tabState, true); } public @NotNull TerminalWidget createNewSession() { @@ -220,10 +232,16 @@ public final class TerminalToolWindowManager implements Disposable { return createNewSession(myTerminalRunner, tabState, requestFocus, deferSessionStartUntilUiShown); } + private void createNewSession(@NotNull AbstractTerminalRunner terminalRunner, + @Nullable TerminalTabState tabState, + boolean requestFocus) { + createNewSession(terminalRunner, tabState, requestFocus, true); + } + private @NotNull TerminalWidget createNewSession(@NotNull AbstractTerminalRunner terminalRunner, - @Nullable TerminalTabState tabState, - boolean requestFocus, - boolean deferSessionStartUntilUiShown) { + @Nullable TerminalTabState tabState, + boolean requestFocus, + boolean deferSessionStartUntilUiShown) { ToolWindow toolWindow = getOrInitToolWindow(); Content content = createNewTab(null, terminalRunner, toolWindow, tabState, requestFocus, deferSessionStartUntilUiShown); return Objects.requireNonNull(content.getUserData(TERMINAL_WIDGET_KEY)); @@ -325,7 +343,7 @@ public final class TerminalToolWindowManager implements Disposable { else { state.setDefaultTitle(tabState.myTabName); } - return Unit.INSTANCE; + return null; }); } updateTabTitle(widget.getTerminalTitle(), toolWindow, content); @@ -474,7 +492,7 @@ public final class TerminalToolWindowManager implements Disposable { content.setDisplayName(generatedName); terminalTitle.change((state) -> { state.setDefaultTitle(generatedName); - return Unit.INSTANCE; + return null; }); } @@ -601,6 +619,16 @@ public final class TerminalToolWindowManager implements Disposable { return content.getUserData(RUNNER_KEY); } + public void detachWidgetAndRemoveContent(@NotNull Content content) { + ContentManager contentManager = myToolWindow.getContentManager(); + LOG.assertTrue(contentManager.getIndexOfContent(content) >= 0, "Not a terminal content"); + TerminalTabCloseListener.Companion.executeContentOperationSilently(content, () -> { + contentManager.removeContent(content, true); + return Unit.INSTANCE; + }); + content.putUserData(TERMINAL_WIDGET_KEY, null); + } + public static boolean isInTerminalToolWindow(@NotNull JBTerminalWidget widget) { DataContext dataContext = DataManager.getInstance().getDataContext(widget.getTerminalPanel()); ToolWindow toolWindow = dataContext.getData(PlatformDataKeys.TOOL_WINDOW); @@ -610,6 +638,49 @@ public final class TerminalToolWindowManager implements Disposable { public static boolean isTerminalToolWindow(@Nullable ToolWindow toolWindow) { return toolWindow != null && TerminalToolWindowFactory.TOOL_WINDOW_ID.equals(toolWindow.getId()); } + + private final class TerminalDockContainer implements DockContainer { + @NotNull + @Override + public RelativeRectangle getAcceptArea() { + return new RelativeRectangle(myToolWindow.getComponent()); + } + + @NotNull + @Override + public ContentResponse getContentResponse(@NotNull DockableContent content, RelativePoint point) { + return isTerminalSessionContent(content) ? ContentResponse.ACCEPT_MOVE : ContentResponse.DENY; + } + + @Override + public @NotNull JComponent getContainerComponent() { + return myToolWindow.getComponent(); + } + + @Override + public void add(@NotNull DockableContent content, RelativePoint dropTarget) { + if (isTerminalSessionContent(content)) { + TerminalSessionVirtualFileImpl terminalFile = (TerminalSessionVirtualFileImpl)content.getKey(); + String name = terminalFile.getName(); + Content newContent = newTab(myToolWindow, terminalFile.getTerminalWidget()); + newContent.setDisplayName(name); + } + } + + private static boolean isTerminalSessionContent(@NotNull DockableContent content) { + return content.getKey() instanceof TerminalSessionVirtualFileImpl; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean isDisposeWhenEmpty() { + return false; + } + } } diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/action/MoveTerminalSessionToEditorAction.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/action/MoveTerminalSessionToEditorAction.kt new file mode 100644 index 000000000000..0e89238c6bf4 --- /dev/null +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/action/MoveTerminalSessionToEditorAction.kt @@ -0,0 +1,34 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.plugins.terminal.action + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.terminal.JBTerminalWidget +import com.intellij.terminal.ui.TerminalWidget +import com.intellij.ui.content.Content +import org.jetbrains.plugins.terminal.TerminalToolWindowManager +import org.jetbrains.plugins.terminal.vfs.TerminalEditorWidgetListener +import org.jetbrains.plugins.terminal.vfs.TerminalSessionVirtualFileImpl + +private class MoveTerminalSessionToEditorAction : TerminalSessionContextMenuActionBase(), DumbAware { + override fun updateInTerminalToolWindow(e: AnActionEvent, project: Project, content: Content, terminalWidget: TerminalWidget) { + e.presentation.isEnabledAndVisible = !TerminalToolWindowManager.getInstance(project).isSplitTerminal(terminalWidget) + } + + override fun actionPerformedInTerminalToolWindow(e: AnActionEvent, project: Project, content: Content, terminalWidget: TerminalWidget) { + val terminalToolWindowManager = TerminalToolWindowManager.getInstance(project) + val file = TerminalSessionVirtualFileImpl(terminalWidget.terminalTitle.buildTitle(), terminalWidget, terminalToolWindowManager.terminalRunner.settingsProvider) + file.putUserData(FileEditorManagerImpl.CLOSING_TO_REOPEN, java.lang.Boolean.TRUE) + FileEditorManager.getInstance(project).openFile(file, true).first() + JBTerminalWidget.asJediTermWidget(terminalWidget)?.let { + it.listener = TerminalEditorWidgetListener(project, file) + } + + terminalToolWindowManager.detachWidgetAndRemoveContent(content) + + file.putUserData(FileEditorManagerImpl.CLOSING_TO_REOPEN, null) + } +} \ No newline at end of file diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/vfs/TerminalEditorWidgetListener.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/vfs/TerminalEditorWidgetListener.kt new file mode 100644 index 000000000000..18a6fc75285c --- /dev/null +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/vfs/TerminalEditorWidgetListener.kt @@ -0,0 +1,27 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.plugins.terminal.vfs + +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.terminal.JBTerminalWidgetListener + +class TerminalEditorWidgetListener(val project: Project, val file: TerminalSessionVirtualFileImpl): JBTerminalWidgetListener { + override fun onNewSession() { + } + + override fun onTerminalStarted() { + } + + override fun onPreviousTabSelected() { + } + + override fun onNextTabSelected() { + } + + override fun onSessionClosed() { + FileEditorManager.getInstance(project).closeFile(file) + } + + override fun showTabs() { + } +} \ No newline at end of file diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/vfs/TerminalSessionEditor.java b/plugins/terminal/src/org/jetbrains/plugins/terminal/vfs/TerminalSessionEditor.java new file mode 100644 index 000000000000..2d29afdad0ff --- /dev/null +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/vfs/TerminalSessionEditor.java @@ -0,0 +1,124 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.plugins.terminal.vfs; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.FileEditorState; +import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx; +import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.UserDataHolderBase; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.terminal.JBTerminalWidget; +import com.intellij.terminal.TerminalTitle; +import com.intellij.terminal.TerminalTitleListener; +import com.intellij.terminal.ui.TerminalWidgetKt; +import com.jediterm.terminal.ui.TerminalWidgetListener; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.beans.PropertyChangeListener; +import java.io.IOException; + +public final class TerminalSessionEditor extends UserDataHolderBase implements FileEditor { + private static final Logger LOG = Logger.getInstance(TerminalSessionEditor.class); + + private final Project myProject; + private final TerminalSessionVirtualFileImpl myFile; + private final TerminalWidgetListener myListener; + private final Disposable myWidgetParentDisposable = Disposer.newDisposable("terminal widget parent"); + + public TerminalSessionEditor(Project project, @NotNull TerminalSessionVirtualFileImpl terminalFile) { + myProject = project; + myFile = terminalFile; + TerminalWidgetKt.setNewParentDisposable(terminalFile.getTerminalWidget(), myWidgetParentDisposable); + + myListener = widget -> { + ApplicationManager.getApplication().invokeLater(() -> { + FileEditorManagerEx.getInstanceEx(myProject).closeFile(myFile); + }, myProject.getDisposed()); + }; + JBTerminalWidget termWidget = JBTerminalWidget.asJediTermWidget(myFile.getTerminalWidget()); + if (termWidget != null) { + termWidget.addListener(myListener); + } + + terminalFile.getTerminalWidget().getTerminalTitle().addTitleListener(new TerminalTitleListener() { + @Override + public void onTitleChanged(@NotNull TerminalTitle terminalTitle) { + try { + terminalFile.rename(null, terminalTitle.buildTitle()); + } + catch (IOException exception) { + throw new RuntimeException("Cannot rename"); + } + FileEditorManager.getInstance(project).updateFilePresentation(terminalFile); + } + }, this); + } + + @Override + public @NotNull JComponent getComponent() { + return myFile.getTerminalWidget().getComponent(); + } + + @Override + public @NotNull JComponent getPreferredFocusedComponent() { + return myFile.getTerminalWidget().getPreferredFocusableComponent(); + } + + @Override + public @NotNull String getName() { + return myFile.getName(); + } + + @Override + public void setState(@NotNull FileEditorState state) { } + + @Override + public boolean isModified() { + return false; + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) { } + + @Override + public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) { } + + @Override + public @NotNull VirtualFile getFile() { + return myFile; + } + + @Override + public void dispose() { + JBTerminalWidget termWidget = JBTerminalWidget.asJediTermWidget(myFile.getTerminalWidget()); + if (termWidget != null) { + termWidget.removeListener(myListener); + } + if (Boolean.TRUE.equals(myFile.getUserData(FileEditorManagerImpl.CLOSING_TO_REOPEN))) { + ApplicationManager.getApplication().invokeLater(() -> { + boolean disposedBefore = Disposer.isDisposed(myFile.getTerminalWidget()); + Disposer.dispose(myWidgetParentDisposable); + boolean disposedAfter = Disposer.isDisposed(myFile.getTerminalWidget()); + if (disposedBefore != disposedAfter) { + LOG.error(JBTerminalWidget.class.getSimpleName() + " parent disposable hasn't been changed " + + "(disposed before: " + disposedBefore + ", disposed after: " + disposedAfter + ")"); + } + }); + } + else { + Disposer.dispose(myWidgetParentDisposable); + } + } +} diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/vfs/TerminalSessionEditorProvider.java b/plugins/terminal/src/org/jetbrains/plugins/terminal/vfs/TerminalSessionEditorProvider.java new file mode 100644 index 000000000000..18fcdef4d9a2 --- /dev/null +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/vfs/TerminalSessionEditorProvider.java @@ -0,0 +1,65 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.plugins.terminal.vfs; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorPolicy; +import com.intellij.openapi.fileEditor.FileEditorProvider; +import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.terminal.ui.TerminalWidget; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.terminal.LocalBlockTerminalRunner; +import org.jetbrains.plugins.terminal.ShellStartupOptions; +import org.jetbrains.plugins.terminal.ShellStartupOptionsKt; +import org.jetbrains.plugins.terminal.arrangement.TerminalWorkingDirectoryManager; + +final class TerminalSessionEditorProvider implements FileEditorProvider, DumbAware { + @Override + public boolean accept(@NotNull Project project, @NotNull VirtualFile file) { + return file instanceof TerminalSessionVirtualFileImpl; + } + + @Override + public boolean acceptRequiresReadAction() { + return false; + } + + @NotNull + @Override + public FileEditor createEditor(@NotNull Project project, @NotNull VirtualFile file) { + TerminalSessionVirtualFileImpl terminalFile = (TerminalSessionVirtualFileImpl)file; + if (file.getUserData(FileEditorManagerImpl.CLOSING_TO_REOPEN) != null) { + return new TerminalSessionEditor(project, terminalFile); + } + else { + TerminalWidget widget = terminalFile.getTerminalWidget(); + + String workingDirectory = TerminalWorkingDirectoryManager.getWorkingDirectory(widget); + Disposable tempDisposable = Disposer.newDisposable(); + ShellStartupOptions options = ShellStartupOptionsKt.shellStartupOptions(workingDirectory); + TerminalWidget newWidget = new LocalBlockTerminalRunner(project).startShellTerminalWidget(tempDisposable, options, true); + TerminalSessionVirtualFileImpl newSessionVirtualFile = new TerminalSessionVirtualFileImpl(terminalFile.getName(), + newWidget, + terminalFile.getSettingsProvider()); + TerminalSessionEditor editor = new TerminalSessionEditor(project, newSessionVirtualFile); + Disposer.dispose(tempDisposable); // newWidget's parent disposable should be changed now + return editor; + } + } + + @NotNull + @Override + public String getEditorTypeId() { + return "terminal-session-editor"; + } + + @NotNull + @Override + public FileEditorPolicy getPolicy() { + return FileEditorPolicy.HIDE_DEFAULT_EDITOR; + } +} diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/vfs/TerminalSessionFileType.java b/plugins/terminal/src/org/jetbrains/plugins/terminal/vfs/TerminalSessionFileType.java new file mode 100644 index 000000000000..0459ac8573f1 --- /dev/null +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/vfs/TerminalSessionFileType.java @@ -0,0 +1,39 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.plugins.terminal.vfs; + +import com.intellij.openapi.fileTypes.ex.FakeFileType; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.terminal.TerminalIcons; + +import javax.swing.*; + +public final class TerminalSessionFileType extends FakeFileType { + + public final static TerminalSessionFileType INSTANCE = new TerminalSessionFileType(); + + private TerminalSessionFileType() { + } + + @Override + @NotNull + public String getName() { + return "Terminal Session"; + } + + @Override + @NotNull + public String getDescription() { + return getName() + " Fake File Type"; //NON-NLS + } + + @Override + public Icon getIcon() { + return TerminalIcons.OpenTerminal_13x13; + } + + @Override + public boolean isMyFileType(@NotNull VirtualFile file) { + return file instanceof TerminalSessionVirtualFileImpl; + } +} diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/vfs/TerminalSessionVirtualFileImpl.java b/plugins/terminal/src/org/jetbrains/plugins/terminal/vfs/TerminalSessionVirtualFileImpl.java new file mode 100644 index 000000000000..8fb759f49abf --- /dev/null +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/vfs/TerminalSessionVirtualFileImpl.java @@ -0,0 +1,37 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.plugins.terminal.vfs; + +import com.intellij.terminal.ui.TerminalWidget; +import com.intellij.testFramework.LightVirtualFile; +import com.jediterm.terminal.ui.settings.SettingsProvider; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; + +public final class TerminalSessionVirtualFileImpl extends LightVirtualFile { + private final TerminalWidget myTerminalWidget; + private final SettingsProvider mySettingsProvider; + + public TerminalSessionVirtualFileImpl(@NotNull String name, + @NotNull TerminalWidget terminalWidget, + @NotNull SettingsProvider settingsProvider) { + myTerminalWidget = terminalWidget; + mySettingsProvider = settingsProvider; + setFileType(TerminalSessionFileType.INSTANCE); + setWritable(true); + try { + rename(null, name); + } + catch (IOException e) { + throw new RuntimeException("Cannot rename"); + } + } + + public @NotNull TerminalWidget getTerminalWidget() { + return myTerminalWidget; + } + + public @NotNull SettingsProvider getSettingsProvider() { + return mySettingsProvider; + } +}