Skip to content

Commit

Permalink
Merge #6563
Browse files Browse the repository at this point in the history
6563: ACT: introduce `Share in Playground` action r=ortem a=Undin

These changes:
* Introduce `Share in Playground` action to share your code in https://play.rust-lang.org/. The action correctly handles selected text in editor (i.e. share only selected text), current edition and toolchain channel.
* Add `Rust` action group to have a single item in context menus for util Rust actions. `Reformat File with Rustfmt`, `Reformat Cargo Project with Rustfmt`, `Rust REPL`, `Share in Playground` are placed into this group as well as `Show expanded Macro` actions. Also, the group is added to `Tools` menu.
* Provide `MockServerFixture` to help setting up mock web server in test to check network interactions

https://user-images.githubusercontent.com/2539310/103181788-265fea80-48b6-11eb-87f7-2ba813cfb596.mov

changelog: Introduce `Share in Playground` action to share your code in https://play.rust-lang.org/. You can invoke it via `Tools | Rust | Share in Playground` or via context menu


Co-authored-by: Arseniy Pendryak <a.pendryak@yandex.ru>
  • Loading branch information
bors[bot] and Undin committed Jan 21, 2021
2 parents 44d8830 + e0f676a commit 25ad5e8
Show file tree
Hide file tree
Showing 11 changed files with 441 additions and 14 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ project(":") {
exclude(module = "kotlin-stdlib")
}
testImplementation(project(":common", "testOutput"))
testImplementation("com.squareup.okhttp3:mockwebserver:4.9.0")
testOutput(sourceSets.getByName("test").output.classesDirs)
}

Expand Down
19 changes: 19 additions & 0 deletions src/main/kotlin/org/rust/ide/actions/RsToolsActionGroup.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Use of this source code is governed by the MIT license that can be
* found in the LICENSE file.
*/

package org.rust.ide.actions

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.project.DumbAware
import org.rust.cargo.runconfig.hasCargoProject

class RsToolsActionGroup : DefaultActionGroup(), DumbAware {

override fun update(e: AnActionEvent) {
val project = e.project ?: return
e.presentation.isEnabledAndVisible = project.hasCargoProject
}
}
161 changes: 161 additions & 0 deletions src/main/kotlin/org/rust/ide/actions/ShareInPlaygroundAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Use of this source code is governed by the MIT license that can be
* found in the LICENSE file.
*/

package org.rust.ide.actions

import com.google.common.annotations.VisibleForTesting
import com.google.gson.Gson
import com.intellij.ide.util.PropertiesComponent
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationListener
import com.intellij.notification.NotificationType
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.Messages
import com.intellij.openapiext.isUnitTestMode
import com.intellij.util.io.HttpRequests
import org.jetbrains.annotations.TestOnly
import org.rust.RsBundle
import org.rust.cargo.project.workspace.CargoWorkspace
import org.rust.ide.notifications.showBalloon
import org.rust.ide.utils.USER_AGENT
import org.rust.lang.core.psi.RsFile
import org.rust.openapiext.JsonUtils
import java.awt.datatransfer.StringSelection

class ShareInPlaygroundAction : DumbAwareAction() {

override fun update(e: AnActionEvent) {
val file = e.getData(CommonDataKeys.PSI_FILE) as? RsFile
e.presentation.isEnabledAndVisible = file != null
}

override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val file = e.getData(CommonDataKeys.PSI_FILE) as? RsFile ?: return
val editor = e.getData(CommonDataKeys.EDITOR)

val (text, hasSelection) = runReadAction {
val selectedText = editor?.selectionModel?.selectedText
if (selectedText != null) {
selectedText to true
} else {
file.text to false
}
}

if (!confirmShare(file, hasSelection)) return

val channel = file.cargoProject?.rustcInfo?.version?.channel?.channel ?: "stable"
val edition = (file.crate?.edition ?: CargoWorkspace.Edition.EDITION_2018).presentation

object : Task.Backgroundable(project, RsBundle.message("action.Rust.ShareInPlayground.progress.title")) {

@Volatile
private var gistId: String? = null

override fun shouldStartInBackground(): Boolean = true
override fun run(indicator: ProgressIndicator) {
val json = Gson().toJson(PlaygroundCode(text))
val response = HttpRequests.post("$playgroundHost/meta/gist/", "application/json")
.userAgent(USER_AGENT)
.connect {
it.write(json)
it.readString(indicator)
}

gistId = JsonUtils.parseJsonObject(response).getAsJsonPrimitive("id").asString
}

override fun onSuccess() {
val url = "https://play.rust-lang.org/?version=$channel&edition=$edition&gist=$gistId"
val copyUrlAction = NotificationAction.createSimple(RsBundle.message("action.Rust.ShareInPlayground.notification.copy.url.text")) {
CopyPasteManager.getInstance().setContents(StringSelection(url))
}
project.showBalloon(
RsBundle.message("action.Rust.ShareInPlayground.notification.title"),
RsBundle.message("action.Rust.ShareInPlayground.notification.text", url),
NotificationType.INFORMATION,
copyUrlAction,
NotificationListener.URL_OPENING_LISTENER
)
}

override fun onThrowable(error: Throwable) {
if (!isUnitTestMode) {
super.onThrowable(error)
}
project.showBalloon(
RsBundle.message("action.Rust.ShareInPlayground.notification.title"),
RsBundle.message("action.Rust.ShareInPlayground.notification.error"),
NotificationType.ERROR
)
}
}.queue()
}

private fun confirmShare(file: RsFile, hasSelection: Boolean): Boolean {
val showConfirmation = PropertiesComponent.getInstance().getBoolean(SHOW_SHARE_IN_PLAYGROUND_CONFIRMATION, true)
if (!showConfirmation) {
return true
}
val doNotAskOption = object : DialogWrapper.DoNotAskOption.Adapter() {
override fun rememberChoice(isSelected: Boolean, exitCode: Int) {
if (isSelected && exitCode == Messages.OK) {
PropertiesComponent.getInstance().setValue(SHOW_SHARE_IN_PLAYGROUND_CONFIRMATION, false, true)
}
}
}

val message = if (hasSelection) {
RsBundle.message("action.Rust.ShareInPlayground.confirmation.selected.text")
} else {
RsBundle.message("action.Rust.ShareInPlayground.confirmation", file.name)
}

val answer = Messages.showOkCancelDialog(
message,
RsBundle.message("action.Rust.ShareInPlayground.text"),
Messages.getOkButton(),
Messages.getCancelButton(),
Messages.getQuestionIcon(),
doNotAskOption
)
return answer == Messages.OK
}

companion object {
private const val SHOW_SHARE_IN_PLAYGROUND_CONFIRMATION = "rs.show.share.in.playground.confirmation"
}

@VisibleForTesting
data class PlaygroundCode(val code: String)
}

private var MOCK: String? = null

private val playgroundHost: String get() {
return if (isUnitTestMode) {
MOCK ?: error("Use `withMockPlaygroundHost`")
} else {
"https://play.rust-lang.org"
}
}

@TestOnly
fun withMockPlaygroundHost(host: String, action: () -> Unit) {
MOCK = host
try {
action()
} finally {
MOCK = null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package org.rust.ide.actions.macroExpansion

import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DefaultActionGroup
import org.rust.lang.core.psi.RsMacroCall
Expand All @@ -16,6 +17,7 @@ import org.rust.lang.core.psi.RsMacroCall
*/
class RsShowMacroExpansionGroup : DefaultActionGroup() {
override fun update(event: AnActionEvent) {
event.presentation.isEnabledAndVisible = getMacroUnderCaret(event.dataContext) != null
val inEditorPopupMenu = event.place == ActionPlaces.EDITOR_POPUP
event.presentation.isEnabledAndVisible = inEditorPopupMenu && getMacroUnderCaret(event.dataContext) != null
}
}
14 changes: 12 additions & 2 deletions src/main/kotlin/org/rust/ide/notifications/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,29 @@
package org.rust.ide.notifications

import com.intellij.notification.NotificationGroup
import com.intellij.notification.NotificationListener
import com.intellij.notification.NotificationType
import com.intellij.notification.Notifications
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.WindowManager

// BACKCOMPAT: 2020.2
@Suppress("DEPRECATION")
private val pluginNotifications = NotificationGroup.balloonGroup("Rust Plugin")

fun Project.showBalloon(content: String, type: NotificationType, action: AnAction? = null) {
showBalloon("", content, type, action)
}
fun Project.showBalloon(title: String, content: String, type: NotificationType, action: AnAction? = null) {
val notification = pluginNotifications.createNotification(title, content, type, null)

fun Project.showBalloon(
title: String,
content: String,
type: NotificationType,
action: AnAction? = null,
listener: NotificationListener? = null
) {
val notification = pluginNotifications.createNotification(title, content, type, listener)
if (action != null) {
notification.addAction(action)
}
Expand Down
8 changes: 8 additions & 0 deletions src/main/kotlin/org/rust/ide/utils/IoUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Use of this source code is governed by the MIT license that can be
* found in the LICENSE file.
*/

package org.rust.ide.utils

const val USER_AGENT = "IntelliJ Rust Plugin (https://github.com/intellij-rust/intellij-rust)"
35 changes: 25 additions & 10 deletions src/main/resources/META-INF/rust-core.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1149,13 +1149,6 @@
<action id="Rust.ShowRecursiveMacroExpansionAction"
class="org.rust.ide.actions.macroExpansion.RsShowRecursiveMacroExpansionAction">
</action>
<group id="Rust.MacroExpansionActions"
class="org.rust.ide.actions.macroExpansion.RsShowMacroExpansionGroup">
<add-to-group group-id="EditorPopupMenu"/>
<separator/>
<reference id="Rust.ShowRecursiveMacroExpansionAction"/>
<reference id="Rust.ShowSingleStepMacroExpansionAction"/>
</group>

<action class="org.rust.ide.refactoring.generate.constructor.GenerateConstructorAction"
id="Rust.GenerateConstructor">
Expand Down Expand Up @@ -1190,6 +1183,9 @@
<add-to-group group-id="HelpMenu" anchor="after" relative-to-action="ReportProblem"/>
</action>

<action id="Rust.ShareInPlayground"
class="org.rust.ide.actions.ShareInPlaygroundAction"/>

<!-- Cargo-->

<action id="Cargo.RustfmtFile"
Expand Down Expand Up @@ -1253,14 +1249,33 @@
<add-to-group group-id="ProjectViewPopupMenu"/>
</group>

<!-- Repl -->

<action id="Rust.ConsoleREPL"
class="org.rust.ide.console.RunRustConsoleAction"
icon="/icons/repl.svg">
<add-to-group group-id="ToolsMenu" anchor="last"/>
</action>

<group id="Rust.Tools"
class="org.rust.ide.actions.RsToolsActionGroup"
icon="/icons/rust.svg"
popup="true">
<group id="Rust.MacroExpansionActions"
class="org.rust.ide.actions.macroExpansion.RsShowMacroExpansionGroup">
<reference id="Rust.ShowRecursiveMacroExpansionAction"/>
<reference id="Rust.ShowSingleStepMacroExpansionAction"/>
<separator/>
</group>

<reference id="Cargo.RustfmtFile"/>
<reference id="Cargo.RustfmtCargoProject"/>
<reference id="Rust.ConsoleREPL"/>
<reference id="Rust.ShareInPlayground"/>

<add-to-group group-id="ToolsMenu" anchor="last"/>
<add-to-group group-id="ProjectViewPopupMenu" anchor="last"/>
<add-to-group group-id="EditorPopupMenu" anchor="last"/>
<add-to-group group-id="EditorTabPopupMenu" anchor="last"/>
</group>

<!-- Internal -->

<action id="Rust.GenerateDictionaries"
Expand Down
11 changes: 11 additions & 0 deletions src/main/resources/messages/RsBundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,19 @@ action.Rust.RsExtractEnumVariant.description=Extract a single enum variant into
action.Rust.RsExtractEnumVariant.text=Extract Enum Variant
action.Rust.RsPromoteModuleToDirectoryAction.description=Move this module to a dedicated directory
action.Rust.RsPromoteModuleToDirectoryAction.text=Promote Module to Directory
action.Rust.ShareInPlayground.confirmation.selected.text=Do you want to upload selected text to the Rust Playground and make it public?
action.Rust.ShareInPlayground.confirmation=Do you want to upload {0} to the Rust Playground and make it public?
action.Rust.ShareInPlayground.description=Share code in Rust Playground (https://play.rust-lang.org/)
action.Rust.ShareInPlayground.notification.copy.url.text=Copy URL to clipboard
action.Rust.ShareInPlayground.notification.error=Failed to share the code in the Rust Playground
action.Rust.ShareInPlayground.notification.text=File shared in the Rust Playground: <a href="{0}">{0}</a>
action.Rust.ShareInPlayground.notification.title=Share in Rust Playground
action.Rust.ShareInPlayground.progress.title=Posting code to Rust Playground
action.Rust.ShareInPlayground.text=Share in Playground

action.Rust.ShowRecursiveMacroExpansionAction.text=Show Recursively Expanded Macro
action.Rust.ShowSingleStepMacroExpansionAction.text=Show Expanded Macro
action.Rust.ToggleNewResolve.text=Rust: Toggle experimental name resolution engine

group.Rust.MacroExpansionActions.text=Show Macro Expansion
group.Rust.Tools.text=Rust
41 changes: 41 additions & 0 deletions src/test/kotlin/org/rust/MockServerFixture.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Use of this source code is governed by the MIT license that can be
* found in the LICENSE file.
*/

package org.rust

import com.intellij.testFramework.fixtures.impl.BaseFixture
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.junit.Assert
import org.rust.ide.utils.USER_AGENT

typealias ResponseHandler = (RecordedRequest) -> MockResponse?

class MockServerFixture : BaseFixture() {

private var handler: ResponseHandler? = null

private val mockWebServer = MockWebServer().apply {
dispatcher = object : Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse {
Assert.assertEquals(USER_AGENT, request.getHeader("User-Agent"))
return handler?.invoke(request) ?: MockResponse().setResponseCode(404)
}
}
}

val baseUrl: String get() = mockWebServer.url("/").toString()

fun withHandler(handler: ResponseHandler) {
this.handler = handler
}

override fun tearDown() {
mockWebServer.shutdown()
super.tearDown()
}
}

0 comments on commit 25ad5e8

Please sign in to comment.