KTNB-790: Fix gist creation for Jupyter Notebooks

Add a relevant test

GitOrigin-RevId: 481a423a37307c978032fe8dcde75e81df134074
This commit is contained in:
Ilya Muradyan
2024-09-12 16:40:57 +02:00
committed by intellij-monorepo-bot
parent 2216387276
commit 9f35bc136e
6 changed files with 204 additions and 144 deletions

View File

@@ -10,6 +10,9 @@
<extensionPoint qualifiedName="intellij.vcs.github.titleAndDescriptionGenerator"
interface="org.jetbrains.plugins.github.pullrequest.ui.toolwindow.create.GHPRTitleAndDescriptionGeneratorExtension"
dynamic="true"/>
<extensionPoint qualifiedName="com.intellij.vcs.github.gistContentsCollector"
interface="org.jetbrains.plugins.github.GithubGistContentsCollector"
dynamic="true"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
@@ -62,6 +65,11 @@
<registryKey defaultValue="5000"
description="Milliseconds margin used when comparing known last seen date with last updated at date for PRs"
key="github.last.seen.state.margin.millis"/>
<vcs.github.gistContentsCollector
implementation="org.jetbrains.plugins.github.DefaultGithubGistContentsCollector"
id="default"
order="last"/>
</extensions>
<extensions defaultExtensionNs="Git4Idea">

View File

@@ -0,0 +1,129 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.github
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.application.WriteAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.vcs.changes.ChangeListManager
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.plugins.github.api.data.request.GithubGistRequest
import org.jetbrains.plugins.github.i18n.GithubBundle
import org.jetbrains.plugins.github.util.GithubNotificationIdsHolder
import org.jetbrains.plugins.github.util.GithubNotifications
import org.jetbrains.plugins.github.util.GithubUtil
import java.io.IOException
open class DefaultGithubGistContentsCollector : GithubGistContentsCollector {
override fun collectContents(gistEventData: GithubGistContentsCollector.GistEventData): List<GithubGistRequest.FileContent> {
val (project, editor, file, files) = gistEventData
if (editor != null) {
val contents = getContentFromEditor(editor, file)
if (contents != null) {
return contents
}
}
val myFiles: Array<VirtualFile>? = files ?: file?.let { arrayOf(it) }
if (myFiles != null) {
return buildList {
for (vf in myFiles) {
addAll(getContentFromFile(vf, project, null))
}
}
}
LOG.error("File, files and editor can't be null all at once!")
throw IllegalStateException("File, files and editor can't be null all at once!")
}
protected open fun getContentFromEditor(editor: Editor, file: VirtualFile?): List<GithubGistRequest.FileContent>? {
val text: String = ReadAction.compute<String?, java.lang.RuntimeException> { editor.selectionModel.selectedText } ?: editor.document.text
if (text.isBlank()) {
return null
}
val fileName = file?.name.orEmpty()
return listOf(GithubGistRequest.FileContent(fileName, text))
}
private fun getContentFromFile(file: VirtualFile, project: Project, prefix: String?): List<GithubGistRequest.FileContent> {
if (file.isDirectory) {
return getContentFromDirectory(file, project, prefix)
}
if (file.fileType.isBinary) {
GithubNotifications.showWarning(project, GithubNotificationIdsHolder.GIST_CANNOT_CREATE,
GithubBundle.message("cannot.create.gist"),
GithubBundle.message("create.gist.error.binary.file", file.name))
return emptyList()
}
val content = WriteAction.computeAndWait<String?, RuntimeException> { getFileContents(file) }
if (content == null) {
GithubNotifications.showWarning(project,
GithubNotificationIdsHolder.GIST_CANNOT_CREATE,
GithubBundle.message("cannot.create.gist"),
GithubBundle.message("create.gist.error.content.read", file.name))
return emptyList()
}
if (content.isBlank()) {
return emptyList()
}
val filename = addPrefix(file.name, prefix, false)
return listOf(GithubGistRequest.FileContent(filename, content))
}
private fun getFileContents(file: VirtualFile): @NlsSafe String? {
try {
return getFileContentInternal(file)
}
catch (e: IOException) {
LOG.info("Couldn't read contents of the file $file", e)
return null
}
}
protected open fun getFileContentInternal(file: VirtualFile): @NlsSafe String? {
val fileDocumentManager = FileDocumentManager.getInstance()
val document = fileDocumentManager.getDocument(file)
if (document != null) {
fileDocumentManager.saveDocument(document)
return document.text
}
else {
return String(file.contentsToByteArray(), file.charset)
}
}
private fun getContentFromDirectory(dir: VirtualFile, project: Project, prefix: String?): List<GithubGistRequest.FileContent> {
val contents: MutableList<GithubGistRequest.FileContent> = ArrayList()
for (file in dir.children) {
if (!isFileIgnored(file, project)) {
val pref = addPrefix(dir.name, prefix, true)
contents.addAll(getContentFromFile(file, project, pref))
}
}
return contents
}
private fun addPrefix(name: String, prefix: String?, addTrailingSlash: Boolean): String {
var pref = prefix ?: ""
pref += name
if (addTrailingSlash) {
pref += "_"
}
return pref
}
private fun isFileIgnored(file: VirtualFile, project: Project): Boolean {
val manager = ChangeListManager.getInstance(project)
return manager.isIgnoredFile(file) || FileTypeManager.getInstance().isFileIgnored(file)
}
companion object {
private val LOG = GithubUtil.LOG
}
}

View File

@@ -2,18 +2,11 @@
package org.jetbrains.plugins.github;
import com.intellij.ide.BrowserUtil;
import com.intellij.notebook.editor.BackedVirtualFile;
import com.intellij.openapi.actionSystem.ActionUpdateThread;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
@@ -21,8 +14,6 @@ import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
@@ -39,14 +30,12 @@ import org.jetbrains.plugins.github.util.*;
import java.awt.datatransfer.StringSelection;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static java.util.Objects.requireNonNull;
public class GithubCreateGistAction extends DumbAwareAction {
private static final Logger LOG = GithubUtil.LOG;
private static final Condition<@Nullable VirtualFile> FILE_WITH_CONTENT = f -> f != null && !(f.getFileType().isBinary());
@Override
@@ -134,7 +123,7 @@ public class GithubCreateGistAction extends DumbAwareAction {
if (token == null) return;
GithubApiRequestExecutor requestExecutor = GithubApiRequestExecutor.Factory.getInstance().create(account.getServer(), token);
List<FileContent> contents = collectContents(project, editor, file, files);
List<FileContent> contents = GithubGistContentsCollector.Companion.collectContents(project, editor, file, files);
if (contents.isEmpty()) return;
String gistUrl = createGist(project, requestExecutor, indicator, account.getServer(),
@@ -176,39 +165,6 @@ public class GithubCreateGistAction extends DumbAwareAction {
return null;
}
@NotNull
static List<FileContent> collectContents(@NotNull Project project,
@Nullable Editor editor,
@Nullable VirtualFile file,
VirtualFile @Nullable [] files) {
boolean isBackedFile = file instanceof BackedVirtualFile;
if (editor != null) {
String content = getContentFromEditor(editor, isBackedFile);
if (content != null) {
if (file != null) {
return Collections.singletonList(new FileContent(file.getName(), content));
}
else {
return Collections.singletonList(new FileContent("", content));
}
}
}
if (files != null) {
List<FileContent> contents = new ArrayList<>();
for (VirtualFile vf : files) {
contents.addAll(getContentFromFile(vf, project, null));
}
return contents;
}
if (file != null) {
return getContentFromFile(file, project, null);
}
LOG.error("File, files and editor can't be null all at once!");
throw new IllegalStateException("File, files and editor can't be null all at once!");
}
@Nullable
static String createGist(@NotNull Project project,
@NotNull GithubApiRequestExecutor executor,
@@ -240,88 +196,4 @@ public class GithubCreateGistAction extends DumbAwareAction {
return null;
}
}
@Nullable
private static String getContentFromEditor(@NotNull final Editor editor, boolean onlySelection) {
String text = ReadAction.compute(() -> editor.getSelectionModel().getSelectedText());
if (text == null && !onlySelection) {
text = editor.getDocument().getText();
}
if (StringUtil.isEmptyOrSpaces(text)) {
return null;
}
return text;
}
@NotNull
private static List<FileContent> getContentFromFile(@NotNull final VirtualFile file, @NotNull Project project, @Nullable String prefix) {
final VirtualFile realFile = BackedVirtualFile.getOriginFileIfBacked(file);
if (realFile.isDirectory()) {
return getContentFromDirectory(realFile, project, prefix);
}
if (realFile.getFileType().isBinary()) {
GithubNotifications
.showWarning(project, GithubNotificationIdsHolder.GIST_CANNOT_CREATE,
GithubBundle.message("cannot.create.gist"),
GithubBundle.message("create.gist.error.binary.file", realFile.getName()));
return Collections.emptyList();
}
String content = WriteAction.computeAndWait(() -> {
try {
FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
Document document = fileDocumentManager.getDocument(realFile);
if (document != null) {
fileDocumentManager.saveDocument(document);
return document.getText();
}
else {
return new String(realFile.contentsToByteArray(), realFile.getCharset());
}
}
catch (IOException e) {
LOG.info("Couldn't read contents of the file " + realFile, e);
return null;
}
});
if (content == null) {
GithubNotifications
.showWarning(project,
GithubNotificationIdsHolder.GIST_CANNOT_CREATE,
GithubBundle.message("cannot.create.gist"),
GithubBundle.message("create.gist.error.content.read", realFile.getName()));
return Collections.emptyList();
}
if (StringUtil.isEmptyOrSpaces(content)) {
return Collections.emptyList();
}
String filename = addPrefix(realFile.getName(), prefix, false);
return Collections.singletonList(new FileContent(filename, content));
}
@NotNull
private static List<FileContent> getContentFromDirectory(@NotNull VirtualFile dir, @NotNull Project project, @Nullable String prefix) {
List<FileContent> contents = new ArrayList<>();
for (VirtualFile file : dir.getChildren()) {
if (!isFileIgnored(file, project)) {
String pref = addPrefix(dir.getName(), prefix, true);
contents.addAll(getContentFromFile(file, project, pref));
}
}
return contents;
}
private static String addPrefix(@NotNull String name, @Nullable String prefix, boolean addTrailingSlash) {
String pref = prefix == null ? "" : prefix;
pref += name;
if (addTrailingSlash) {
pref += "_";
}
return pref;
}
private static boolean isFileIgnored(@NotNull VirtualFile file, @NotNull Project project) {
ChangeListManager manager = ChangeListManager.getInstance(project);
return manager.isIgnoredFile(file) || FileTypeManager.getInstance().isFileIgnored(file);
}
}

View File

@@ -0,0 +1,41 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.github
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.plugins.github.api.data.request.GithubGistRequest
interface GithubGistContentsCollector {
data class GistEventData(
val project: Project,
val editor: Editor?,
val file: VirtualFile?,
val files: Array<VirtualFile>?,
)
/**
* Collects the contents of a GitHub Gist based on the provided event data.
*
* @param gistEventData Data related to the Gist event, including project reference, editor instance, and target files.
* @return A list of file contents for the Gist request.
* Null indicates that this collector isn't responsible for this type of data, and the content should be passed to the next collector.
* Empty list means that the collector is aware of this type of data but wasn't able to collect any content.
*/
fun collectContents(gistEventData: GistEventData): List<GithubGistRequest.FileContent>?
companion object {
val EP = ExtensionPointName.create<GithubGistContentsCollector>("com.intellij.vcs.github.gistContentsCollector")
fun collectContents(
project: Project,
editor: Editor?,
file: VirtualFile?,
files: Array<VirtualFile>?,
): List<GithubGistRequest.FileContent> {
val eventData = GistEventData(project, editor, file, files)
return EP.extensionList.firstNotNullOf { it.collectContents(eventData) }
}
}
}

View File

@@ -26,7 +26,7 @@ class GithubCreateGistContentTest : GithubCreateGistContentTestBase() {
val file = projectRoot.findFileByRelativePath("file.txt")
assertNotNull(file)
val actual = GithubCreateGistAction.collectContents(myProject, null, file, null)
val actual = collectContents(myProject, null, file, null)
checkEquals(expected, actual)
}
@@ -40,7 +40,7 @@ class GithubCreateGistContentTest : GithubCreateGistContentTestBase() {
val file = projectRoot.findFileByRelativePath("folder")
assertNotNull(file)
val actual = GithubCreateGistAction.collectContents(myProject, null, file, null)
val actual = collectContents(myProject, null, file, null)
checkEquals(expected, actual)
}
@@ -51,7 +51,7 @@ class GithubCreateGistContentTest : GithubCreateGistContentTestBase() {
val file = projectRoot.findFileByRelativePath("folder/empty_folder")
assertNotNull(file)
val actual = GithubCreateGistAction.collectContents(myProject, null, file, null)
val actual = collectContents(myProject, null, file, null)
checkEquals(expected, actual)
}
@@ -62,7 +62,7 @@ class GithubCreateGistContentTest : GithubCreateGistContentTestBase() {
val file = projectRoot.findFileByRelativePath("folder/empty_file")
assertNotNull(file)
val actual = GithubCreateGistAction.collectContents(myProject, null, file, null)
val actual = collectContents(myProject, null, file, null)
checkEquals(expected, actual)
}
@@ -73,15 +73,13 @@ class GithubCreateGistContentTest : GithubCreateGistContentTestBase() {
expected.add(FileContent("file2", "file2 content"))
expected.add(FileContent("file3", "file3 content"))
val files = arrayOfNulls<VirtualFile>(3)
files[0] = projectRoot.findFileByRelativePath("file.txt")
files[1] = projectRoot.findFileByRelativePath("folder/file2")
files[2] = projectRoot.findFileByRelativePath("folder/dir/file3")
assertNotNull(files[0])
assertNotNull(files[1])
assertNotNull(files[2])
val files = arrayOf(
projectRoot.findFileByRelativePath("file.txt")!!,
projectRoot.findFileByRelativePath("folder/file2")!!,
projectRoot.findFileByRelativePath("folder/dir/file3")!!,
)
val actual = GithubCreateGistAction.collectContents(myProject, null, null, files)
val actual = collectContents(myProject, null, null, files)
checkEquals(expected, actual)
}
@@ -91,7 +89,7 @@ class GithubCreateGistContentTest : GithubCreateGistContentTestBase() {
val files = VirtualFile.EMPTY_ARRAY
val actual = GithubCreateGistAction.collectContents(myProject, null, null, files)
val actual = collectContents(myProject, null, null, files)
checkEquals(expected, actual)
}
@@ -109,7 +107,7 @@ class GithubCreateGistContentTest : GithubCreateGistContentTestBase() {
val expected = ArrayList<FileContent>()
expected.add(FileContent("file.txt", "file.txt content"))
val actual = GithubCreateGistAction.collectContents(myProject, editor, file, null)
val actual = collectContents(myProject, editor, file, null)
checkEquals(expected, actual)
}
@@ -127,7 +125,7 @@ class GithubCreateGistContentTest : GithubCreateGistContentTestBase() {
val expected = ArrayList<FileContent>()
expected.add(FileContent("", "file.txt content"))
val actual = GithubCreateGistAction.collectContents(myProject, editor, null, null)
val actual = collectContents(myProject, editor, null, null)
checkEquals(expected, actual)
}

View File

@@ -1,7 +1,10 @@
// 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.github
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Comparing
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.plugins.github.api.data.request.GithubGistRequest.FileContent
import org.jetbrains.plugins.github.test.GithubTest
@@ -15,4 +18,13 @@ abstract class GithubCreateGistContentTestBase : GithubTest() {
protected fun checkEquals(expected: List<FileContent>, actual: List<FileContent>) {
assertTrue("Gist content differs from sample", Comparing.haveEqualElements(expected, actual))
}
protected fun collectContents(
project: Project,
editor: Editor?,
file: VirtualFile?,
files: Array<VirtualFile>?,
): List<FileContent> {
return GithubGistContentsCollector.collectContents(project, editor, file, files)
}
}