mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJPL-246739 Add in-IDE WSL user satisfaction survey
Adds a feedback survey targeted at users actively working with WSL to measure satisfaction with WSL support (performance and freezes in particular) and gather areas for improvement. `WslSatisfactionSurveyStore` persists a distinct-days counter (incremented at most once per day by `WslSatisfactionSurveyProjectActivity` on WSL project open) and the first-WSL-use timestamp; `shouldShowDialog()` requires the project to have been opened in WSL on at least 3 distinct days and at least 3 days since first WSL use, matching the "opened in WSL x3 / after 3-5 days" brief. The survey shows only on Windows in IDEA, PyCharm, WebStorm, PhpStorm and GoLand, only for version 2026.2(.x), and only for existing users (`InitialConfigImportState.isNewUser()` is excluded). Feedback auto-attaches IDE name, WSL distribution and version, and whether WSL runs via IJent; freezes are covered by the survey questions rather than auto-detected. Registry keys `wsl.survey.first.use.date` and `wsl.survey.days.with.wsl` allow testing the show conditions, and an internal action shows the notification (and, via its "Respond" action, the dialog) as a test request. The dialog uses the platform feedback UI blocks: a star `RatingBlock` for overall satisfaction and a star `RatingGroupBlock` for stability, responsiveness and overall quality, dropdowns for the two freeze-frequency questions, and a two-column checkbox group for where freezes occur. To support this, `RatingBlock` gains an optional hint, `ComboBoxBlock` gains `useWrappingLabel()` so a long question wraps instead of stretching the dialog, and `CheckBoxGroupBlock` gains `setColumnCount()` to lay options out over several columns (isolated in its own grid so it does not affect sibling blocks). (cherry picked from commit 52635e9c8bd195ba3f97ef5f9f2f5cec72588d22) IJ-MR-214055 GitOrigin-RevId: c5ee3239cfa3463e846173c758652c0615677fb8
This commit is contained in:
committed by
intellij-monorepo-bot
parent
e2a6d9082d
commit
aae05ffee9
@@ -21,6 +21,7 @@ jvm_library(
|
||||
deps = [
|
||||
# do not sort,
|
||||
"//platform/core-api:core",
|
||||
"//platform/execution",
|
||||
"//platform/projectModel-api:projectModel",
|
||||
"//platform/util",
|
||||
"//platform/editor-ui-api:editor-ui",
|
||||
@@ -56,6 +57,7 @@ jvm_library(
|
||||
# do not sort,
|
||||
":feedback",
|
||||
"//platform/core-api:core_test_lib",
|
||||
"//platform/execution:execution_test_lib",
|
||||
"//platform/projectModel-api:projectModel_test_lib",
|
||||
"//platform/util:util_test_lib",
|
||||
"//platform/editor-ui-api:editor-ui_test_lib",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="module" module-name="intellij.platform.core" />
|
||||
<orderEntry type="module" module-name="intellij.platform.execution" />
|
||||
<orderEntry type="module" module-name="intellij.platform.projectModel" />
|
||||
<orderEntry type="module" module-name="intellij.platform.util" />
|
||||
<orderEntry type="module" module-name="intellij.platform.editor.ui" />
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<module name="intellij.platform.core.impl"/>
|
||||
<module name="intellij.platform.core.ui"/>
|
||||
<module name="intellij.platform.editor.ui"/>
|
||||
<module name="intellij.platform.execution"/>
|
||||
<module name="intellij.platform.ide.core"/>
|
||||
<module name="intellij.platform.ide.impl"/>
|
||||
<module name="intellij.platform.lang"/>
|
||||
@@ -69,6 +70,14 @@
|
||||
<feedback.idleFeedbackSurvey implementation="com.intellij.platform.feedback.editor.ux.EditorUxFeedbackSurvey"/>
|
||||
<registryKey key="editor.ux.survey.enabled" defaultValue="false" description="Enables Editor UX survey for 10% users"/>
|
||||
|
||||
<feedback.idleFeedbackSurvey implementation="com.intellij.platform.feedback.wsl.WslSatisfactionSurvey"/>
|
||||
<backgroundPostStartupActivity implementation="com.intellij.platform.feedback.wsl.WslSatisfactionSurveyProjectActivity"/>
|
||||
<applicationService serviceImplementation="com.intellij.platform.feedback.wsl.WslSatisfactionSurveyStore"/>
|
||||
<registryKey key="wsl.survey.first.use.date" defaultValue=""
|
||||
description="ISO Formatted date of first WSL usage such as 2026-07-15 (for testing the WSL satisfaction survey)"/>
|
||||
<registryKey key="wsl.survey.days.with.wsl" defaultValue="-1"
|
||||
description="Overrides the number of days a WSL project was open (for testing the WSL satisfaction survey); -1 disables the override"/>
|
||||
|
||||
<registryKey key="csat.survey.enabled" defaultValue="true" description="Enables periodic CSAT survey"/>
|
||||
<registryKey key="csat.survey.today" defaultValue="" description="ISO Formatted today date such as 2011-12-03"/>
|
||||
<registryKey key="csat.survey.user.created.date" defaultValue="" description="ISO Formatted date when user created such as 2011-12-03"/>
|
||||
@@ -78,6 +87,7 @@
|
||||
<applicationSettings service="com.intellij.platform.feedback.localization.LocalizationFeedbackService"/>
|
||||
<applicationSettings service="com.intellij.platform.feedback.impl.state.DontShowAgainFeedbackService"/>
|
||||
<applicationSettings service="com.intellij.platform.feedback.impl.state.CommonFeedbackSurveyService"/>
|
||||
<applicationSettings service="com.intellij.platform.feedback.wsl.WslSatisfactionSurveyStore"/>
|
||||
</extensions>
|
||||
|
||||
<actions>
|
||||
@@ -96,6 +106,9 @@
|
||||
<action id="Editor.ShowEditorUxFeedbackNotification"
|
||||
class="com.intellij.platform.feedback.editor.ux.ShowEditorUxFeedbackNotificationAction"
|
||||
internal="true"/>
|
||||
<action id="ShowWslSatisfactionDialog"
|
||||
class="com.intellij.platform.feedback.wsl.ShowWslSatisfactionDialogAction"
|
||||
internal="true"/>
|
||||
<add-to-group group-id="Internal.UI" anchor="last"/>
|
||||
</group>
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
action.ShowWslSatisfactionDialog.text=Show WSL Satisfaction Notification
|
||||
|
||||
wsl.satisfaction.notification.request.title=WSL feedback
|
||||
wsl.satisfaction.notification.request.content=Please help us improve the WSL experience by taking our short survey.
|
||||
|
||||
wsl.satisfaction.dialog.top.title=WSL Support Experience Survey
|
||||
wsl.satisfaction.dialog.title=WSL support experience survey
|
||||
wsl.satisfaction.dialog.description=We are working on improving WSL support in {0} and would like to hear about your recent experience.
|
||||
|
||||
wsl.satisfaction.dialog.rating.label=How satisfied are you with WSL support?
|
||||
wsl.satisfaction.dialog.rating.hint=Where 1 is "very dissatisfied" and 5 is "very satisfied".
|
||||
|
||||
wsl.satisfaction.dialog.areas.label=How would you rate the following aspects of WSL support?
|
||||
wsl.satisfaction.dialog.areas.hint=Where 1 is "poor" and 5 is "excellent".
|
||||
wsl.satisfaction.dialog.area.stability=Stability (freezes)
|
||||
wsl.satisfaction.dialog.area.responsiveness=Responsiveness
|
||||
wsl.satisfaction.dialog.area.quality=Overall quality
|
||||
|
||||
wsl.satisfaction.dialog.freeze.comparison.label=Compared to your previous experience, how often does the IDE freeze, hang, or become unresponsive when working with WSL in the current version?
|
||||
wsl.satisfaction.dialog.freeze.comparison.less=Less often
|
||||
wsl.satisfaction.dialog.freeze.comparison.same=About the same as before
|
||||
wsl.satisfaction.dialog.freeze.comparison.more=More often
|
||||
wsl.satisfaction.dialog.freeze.comparison.unsure=Hard to say
|
||||
|
||||
wsl.satisfaction.dialog.freeze.frequency.label=How often do you currently experience IDE freezes when working with WSL?
|
||||
wsl.satisfaction.dialog.freeze.frequency.several.day=Several times a day
|
||||
wsl.satisfaction.dialog.freeze.frequency.once.day=About once a day
|
||||
wsl.satisfaction.dialog.freeze.frequency.several.week=Several times a week
|
||||
wsl.satisfaction.dialog.freeze.frequency.monthly=Once or several times a month
|
||||
wsl.satisfaction.dialog.freeze.frequency.rarely=Rarely or almost never
|
||||
|
||||
wsl.satisfaction.dialog.freeze.situations.label=When do freezes usually occur, if at all?
|
||||
wsl.satisfaction.dialog.freeze.situations.none=I don't encounter freezes
|
||||
wsl.satisfaction.dialog.freeze.situations.opening=Opening a WSL project
|
||||
wsl.satisfaction.dialog.freeze.situations.indexing=Indexing
|
||||
wsl.satisfaction.dialog.freeze.situations.running=Running or debugging
|
||||
wsl.satisfaction.dialog.freeze.situations.terminal=Using the terminal
|
||||
wsl.satisfaction.dialog.freeze.situations.vcs=Git/VCS operations
|
||||
wsl.satisfaction.dialog.freeze.situations.docker=Docker or containers
|
||||
wsl.satisfaction.dialog.freeze.situations.editing=Editing files
|
||||
wsl.satisfaction.dialog.freeze.situations.branches=Switching branches
|
||||
wsl.satisfaction.dialog.freeze.situations.search=Searching/navigation
|
||||
|
||||
wsl.satisfaction.dialog.additional.label=Is there anything else you would like to tell us about WSL support in {0}?
|
||||
|
||||
wsl.satisfaction.thanks.feedback.content=Thank you for your feedback! It will help us improve WSL support.
|
||||
|
||||
wsl.satisfaction.system.info.ide.name=IDE name:
|
||||
wsl.satisfaction.system.info.distribution=WSL distribution:
|
||||
wsl.satisfaction.system.info.wsl.version=WSL version:
|
||||
wsl.satisfaction.system.info.eel.api.used=WSL via IJent:
|
||||
+50
-13
@@ -11,6 +11,7 @@ import com.intellij.ui.dsl.builder.BottomGap
|
||||
import com.intellij.ui.dsl.builder.Cell
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.ui.dsl.builder.RightGap
|
||||
import com.intellij.ui.dsl.builder.Row
|
||||
import com.intellij.ui.dsl.builder.bindSelected
|
||||
import com.intellij.ui.dsl.builder.bindText
|
||||
import com.intellij.ui.layout.selected
|
||||
@@ -34,6 +35,7 @@ class CheckBoxGroupBlock(
|
||||
private var myIncludeOtherTextField = false
|
||||
private var myOtherProperty: String = ""
|
||||
private var myOtherTextfieldPlaceholderText: String = ""
|
||||
private var myColumnCount: Int = 1
|
||||
|
||||
private var otherCheckBox: JBCheckBox? = null
|
||||
private var otherTextField: JBTextField? = null
|
||||
@@ -41,23 +43,31 @@ class CheckBoxGroupBlock(
|
||||
override fun addToPanel(panel: Panel) {
|
||||
val allCheckBoxes = mutableListOf<JBCheckBox>()
|
||||
|
||||
panel.apply {
|
||||
buttonsGroup(indent = false) {
|
||||
fun Row.addCheckBox(itemData: CheckBoxItemData) {
|
||||
allCheckBoxes += checkBox(itemData.label)
|
||||
.bindSelected({ itemData.property }, { itemData.property = it })
|
||||
.addChooseOptionValidation(allCheckBoxes)
|
||||
.component
|
||||
}
|
||||
|
||||
val itemRows = myItemsData.chunked(myColumnCount)
|
||||
|
||||
val groupContent: (Panel) -> Unit = { container ->
|
||||
container.buttonsGroup(indent = false) {
|
||||
row {
|
||||
label(myGroupLabel)
|
||||
.bold()
|
||||
}.bottomGap(BottomGap.NONE)
|
||||
myItemsData.forEachIndexed { i, itemData ->
|
||||
row {
|
||||
allCheckBoxes += checkBox(itemData.label).bindSelected(
|
||||
{ myItemsData[i].property },
|
||||
{ myItemsData[i].property = it }
|
||||
).addChooseOptionValidation(allCheckBoxes)
|
||||
.component
|
||||
}.apply {
|
||||
if (i == myItemsData.size - 1 && !myIncludeOtherTextField) {
|
||||
this.bottomGap(BottomGap.MEDIUM)
|
||||
}
|
||||
itemRows.forEachIndexed { rowIndex, rowItems ->
|
||||
// Use the standard multi-column DSL helpers so the checkboxes align in evenly spaced columns.
|
||||
val cells: List<(Row.() -> Unit)?> = rowItems.map { itemData -> { addCheckBox(itemData) } }
|
||||
val addedRow = when (myColumnCount) {
|
||||
2 -> twoColumnsRow(cells.getOrNull(0), cells.getOrNull(1))
|
||||
3 -> threeColumnsRow(cells.getOrNull(0), cells.getOrNull(1), cells.getOrNull(2))
|
||||
else -> row { rowItems.forEach { addCheckBox(it) } }
|
||||
}
|
||||
if (rowIndex == itemRows.lastIndex && !myIncludeOtherTextField) {
|
||||
addedRow.bottomGap(BottomGap.MEDIUM)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +119,17 @@ class CheckBoxGroupBlock(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
panel.apply {
|
||||
if (myColumnCount > 1) {
|
||||
// Isolate the multi-column grid in its own sub-panel so its wide columns do not force sibling
|
||||
// blocks (e.g. rating rows) to align with them.
|
||||
panel { groupContent(this) }
|
||||
}
|
||||
else {
|
||||
groupContent(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun collectBlockTextDescription(stringBuilder: StringBuilder) {
|
||||
@@ -151,6 +172,22 @@ class CheckBoxGroupBlock(
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Lays the checkboxes out over the given number of [columnCount] columns (1, 2 or 3) to keep tall option
|
||||
* lists compact, using the standard [Panel.twoColumnsRow]/[Panel.threeColumnsRow] layout.
|
||||
*
|
||||
* Follow the IntelliJ UI guidelines when choosing the value:
|
||||
* arrange checkboxes with labels of up to 30 characters in 2 columns,
|
||||
* and checkboxes with labels of up to 15 characters in 3 columns.
|
||||
*
|
||||
* @see <a href="https://plugins.jetbrains.com/docs/intellij/layout.html#checkboxes-and-radio-buttons">Layout guidelines</a>
|
||||
*/
|
||||
fun setColumnCount(columnCount: Int): CheckBoxGroupBlock {
|
||||
require(columnCount in 1..3) { "columnCount must be 1, 2 or 3, but was $columnCount" }
|
||||
myColumnCount = columnCount
|
||||
return this
|
||||
}
|
||||
|
||||
private fun <T : JBCheckBox> Cell<T>.addChooseOptionValidation(allCheckBoxes: List<JBCheckBox>): Cell<T> {
|
||||
if (requireAnswer) {
|
||||
val errorMessage = if (myIncludeOtherTextField) {
|
||||
|
||||
+24
-1
@@ -8,10 +8,12 @@ import com.intellij.platform.feedback.impl.bundle.CommonFeedbackBundle
|
||||
import com.intellij.ui.dsl.builder.Align
|
||||
import com.intellij.ui.dsl.builder.BottomGap
|
||||
import com.intellij.ui.dsl.builder.LabelPosition
|
||||
import com.intellij.ui.dsl.builder.MAX_LINE_LENGTH_WORD_WRAP
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.ui.dsl.builder.bindItem
|
||||
import com.intellij.ui.dsl.builder.columns
|
||||
import com.intellij.ui.dsl.builder.toMutableProperty
|
||||
import com.intellij.xml.util.XmlStringUtil
|
||||
import kotlinx.serialization.json.JsonObjectBuilder
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
@@ -24,14 +26,26 @@ class ComboBoxBlock(@NlsContexts.Label private val myLabel: String,
|
||||
private var myColumnSize: Int = COMBOBOX_COLUMN_SIZE
|
||||
private var myRandomizeOptionOrder: Boolean = false
|
||||
private var myUseAlignFill: Boolean = false
|
||||
private var myUseWrappingLabel: Boolean = false
|
||||
|
||||
override fun addToPanel(panel: Panel) {
|
||||
val items = if (myRandomizeOptionOrder) myItems.shuffled() else myItems
|
||||
|
||||
panel.apply {
|
||||
// A long label rendered as a single-line JBLabel forces the whole dialog to become very wide,
|
||||
// so it can optionally be shown as a word-wrapped label above the combo box instead.
|
||||
if (myUseWrappingLabel) {
|
||||
row {
|
||||
text("<b>${XmlStringUtil.escapeString(myLabel)}</b>", maxLineLength = MAX_LINE_LENGTH_WORD_WRAP)
|
||||
}.bottomGap(BottomGap.NONE)
|
||||
}
|
||||
row {
|
||||
comboBox(items)
|
||||
.label(createBoldJBLabel(myLabel), LabelPosition.TOP)
|
||||
.apply {
|
||||
if (!myUseWrappingLabel) {
|
||||
label(createBoldJBLabel(myLabel), LabelPosition.TOP)
|
||||
}
|
||||
}
|
||||
.bindItem(::myProperty.toMutableProperty())
|
||||
.apply {
|
||||
if (myUseAlignFill) {
|
||||
@@ -82,6 +96,15 @@ class ComboBoxBlock(@NlsContexts.Label private val myLabel: String,
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the label as a word-wrapped text above the combo box instead of a single-line label,
|
||||
* so a long question does not stretch the whole dialog horizontally.
|
||||
*/
|
||||
fun useWrappingLabel(): ComboBoxBlock {
|
||||
myUseWrappingLabel = true
|
||||
return this
|
||||
}
|
||||
|
||||
fun randomizeOptionOrder(): ComboBoxBlock {
|
||||
myRandomizeOptionOrder = true
|
||||
return this
|
||||
|
||||
+25
-3
@@ -12,18 +12,35 @@ import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.ui.dsl.builder.Row
|
||||
import kotlinx.serialization.json.JsonObjectBuilder
|
||||
import kotlinx.serialization.json.put
|
||||
import org.jetbrains.annotations.Nls
|
||||
|
||||
class RatingBlock(@NlsContexts.Label private val myLabel: String,
|
||||
private val myJsonElementName: String) : FeedbackBlock, TextDescriptionProvider, JsonDataProvider {
|
||||
class RatingBlock(
|
||||
@NlsContexts.Label private val myLabel: String,
|
||||
private val myJsonElementName: String,
|
||||
) : FeedbackBlock, TextDescriptionProvider, JsonDataProvider {
|
||||
|
||||
private var myProperty: Int = 0
|
||||
private var requireAnswer: Boolean = true
|
||||
private var myHint: @Nls String? = null
|
||||
|
||||
override fun addToPanel(panel: Panel) {
|
||||
val hint = myHint
|
||||
panel.apply {
|
||||
// With a hint, show the label and the hint above the rating (like RatingGroupBlock);
|
||||
// otherwise keep the label attached on top of the rating for backward compatibility.
|
||||
if (hint != null) {
|
||||
row {
|
||||
label(myLabel).bold()
|
||||
rowComment(hint)
|
||||
}.bottomGap(BottomGap.NONE)
|
||||
}
|
||||
row {
|
||||
rating()
|
||||
.label(createBoldJBLabel(myLabel), LabelPosition.TOP)
|
||||
.apply {
|
||||
if (hint == null) {
|
||||
label(createBoldJBLabel(myLabel), LabelPosition.TOP)
|
||||
}
|
||||
}
|
||||
.apply {
|
||||
onApply {
|
||||
myProperty = this.component.myRating
|
||||
@@ -57,6 +74,11 @@ class RatingBlock(@NlsContexts.Label private val myLabel: String,
|
||||
requireAnswer = false
|
||||
return this
|
||||
}
|
||||
|
||||
fun setHint(@Nls hint: String): RatingBlock {
|
||||
myHint = hint
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Row.rating(): Cell<RatingComponent> {
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.platform.feedback.wsl
|
||||
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import java.util.function.Supplier
|
||||
|
||||
/**
|
||||
* Shows the WSL satisfaction survey notification (bypassing the usual show conditions) so QA can check
|
||||
* both the notification wording and, via the "Respond" action, the dialog. Feedback is sent as a test request.
|
||||
*/
|
||||
@Suppress("ActionPresentationInstantiatedInCtor")
|
||||
internal class ShowWslSatisfactionDialogAction :
|
||||
DumbAwareAction(Supplier { WslSatisfactionFeedbackBundle.message("action.ShowWslSatisfactionDialog.text") }) {
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val project = e.project ?: return
|
||||
WslSatisfactionSurvey().showNotification(project, forTest = true)
|
||||
}
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
e.presentation.isEnabledAndVisible = e.project != null
|
||||
}
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.platform.feedback.wsl
|
||||
|
||||
import com.intellij.DynamicBundle
|
||||
import org.jetbrains.annotations.Nls
|
||||
import org.jetbrains.annotations.NonNls
|
||||
import org.jetbrains.annotations.PropertyKey
|
||||
|
||||
internal object WslSatisfactionFeedbackBundle {
|
||||
|
||||
@NonNls
|
||||
private const val BUNDLE = "messages.WslSatisfactionFeedbackBundle"
|
||||
|
||||
private val INSTANCE = DynamicBundle(WslSatisfactionFeedbackBundle::class.java, BUNDLE)
|
||||
|
||||
@Nls
|
||||
fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any): String {
|
||||
return INSTANCE.getMessage(key, *params)
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.platform.feedback.wsl
|
||||
|
||||
import com.intellij.execution.wsl.WSLDistribution
|
||||
import com.intellij.execution.wsl.WslDistributionManager
|
||||
import com.intellij.execution.wsl.WslIjentAvailabilityService
|
||||
import com.intellij.openapi.application.ApplicationNamesInfo
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.wm.WindowManager
|
||||
import com.intellij.platform.feedback.dialog.BlockBasedFeedbackDialogWithEmail
|
||||
import com.intellij.platform.feedback.dialog.CommonFeedbackSystemData
|
||||
import com.intellij.platform.feedback.dialog.showFeedbackSystemInfoDialog
|
||||
import com.intellij.platform.feedback.dialog.uiBlocks.CheckBoxGroupBlock
|
||||
import com.intellij.platform.feedback.dialog.uiBlocks.CheckBoxItemData
|
||||
import com.intellij.platform.feedback.dialog.uiBlocks.ComboBoxBlock
|
||||
import com.intellij.platform.feedback.dialog.uiBlocks.DescriptionBlock
|
||||
import com.intellij.platform.feedback.dialog.uiBlocks.FeedbackBlock
|
||||
import com.intellij.platform.feedback.dialog.uiBlocks.RatingBlock
|
||||
import com.intellij.platform.feedback.dialog.uiBlocks.RatingGroupBlock
|
||||
import com.intellij.platform.feedback.dialog.uiBlocks.RatingItem
|
||||
import com.intellij.platform.feedback.dialog.uiBlocks.TextAreaBlock
|
||||
import com.intellij.platform.feedback.dialog.uiBlocks.TopLabelBlock
|
||||
import com.intellij.platform.feedback.impl.notification.ThanksForFeedbackNotification
|
||||
import com.intellij.ui.ScreenUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.awt.Dimension
|
||||
import java.awt.Rectangle
|
||||
import java.nio.file.Path
|
||||
import javax.swing.JComponent
|
||||
|
||||
internal class WslSatisfactionFeedbackDialog(
|
||||
private val project: Project,
|
||||
forTest: Boolean,
|
||||
) : BlockBasedFeedbackDialogWithEmail<WslSatisfactionSystemData>(project, forTest) {
|
||||
|
||||
/** Increase the additional number when feedback format is changed */
|
||||
override val myFeedbackJsonVersion: Int = super.myFeedbackJsonVersion + 1
|
||||
|
||||
override val myFeedbackReportId: String = "wsl_satisfaction"
|
||||
override val zendeskTicketTitle: String = "WSL Support in-IDE Feedback"
|
||||
override val zendeskFeedbackType: String = "WSL Support Feedback"
|
||||
override fun shouldAutoCloseZendeskTicket(): Boolean = false
|
||||
|
||||
private val ideName: String = ApplicationNamesInfo.getInstance().fullProductName
|
||||
|
||||
override suspend fun computeSystemInfoData(): WslSatisfactionSystemData {
|
||||
val distribution = withContext(Dispatchers.IO) { findWslDistribution(project.basePath) }
|
||||
val isEelApiUsed = WslIjentAvailabilityService.getInstance().runWslCommandsViaIjent()
|
||||
return WslSatisfactionSystemData(
|
||||
ideName = ideName,
|
||||
wslDistribution = distribution?.presentableName ?: "UNKNOWN",
|
||||
wslVersion = distribution?.version ?: -1,
|
||||
isEelApiUsed = isEelApiUsed,
|
||||
commonData = CommonFeedbackSystemData.getCurrentData(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun findWslDistribution(projectPath: String?): WSLDistribution? {
|
||||
if (projectPath == null) return null
|
||||
return WslDistributionManager.getInstance().installedDistributions.firstOrNull {
|
||||
runCatching { Path.of(projectPath).startsWith(Path.of(it.getUNCRootPath().toUri())) }.getOrDefault(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun showFeedbackSystemInfoDialog(systemInfoData: WslSatisfactionSystemData) {
|
||||
showFeedbackSystemInfoDialog(myProject, systemInfoData.commonData) {
|
||||
row(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.system.info.ide.name")) {
|
||||
label(systemInfoData.ideName)
|
||||
}
|
||||
row(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.system.info.distribution")) {
|
||||
label(systemInfoData.wslDistribution)
|
||||
}
|
||||
row(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.system.info.wsl.version")) {
|
||||
label(systemInfoData.wslVersion.toString())
|
||||
}
|
||||
row(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.system.info.eel.api.used")) {
|
||||
label(systemInfoData.isEelApiUsed.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val myTitle: String = WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.top.title")
|
||||
|
||||
override val myBlocks: List<FeedbackBlock> = listOf(
|
||||
TopLabelBlock(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.title")),
|
||||
DescriptionBlock(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.description", ideName)),
|
||||
|
||||
RatingBlock(
|
||||
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.rating.label"),
|
||||
"satisfaction",
|
||||
).setHint(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.rating.hint")),
|
||||
|
||||
RatingGroupBlock(
|
||||
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.areas.label"),
|
||||
listOf(
|
||||
RatingItem(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.area.stability"), "stability"),
|
||||
RatingItem(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.area.responsiveness"), "responsiveness"),
|
||||
RatingItem(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.area.quality"), "overall_quality"),
|
||||
),
|
||||
).setHint(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.areas.hint")),
|
||||
|
||||
ComboBoxBlock(
|
||||
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.comparison.label"),
|
||||
listOf(
|
||||
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.comparison.less"),
|
||||
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.comparison.same"),
|
||||
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.comparison.more"),
|
||||
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.comparison.unsure"),
|
||||
),
|
||||
"freeze_comparison",
|
||||
).useWrappingLabel(),
|
||||
|
||||
ComboBoxBlock(
|
||||
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.frequency.label"),
|
||||
listOf(
|
||||
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.frequency.several.day"),
|
||||
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.frequency.once.day"),
|
||||
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.frequency.several.week"),
|
||||
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.frequency.monthly"),
|
||||
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.frequency.rarely"),
|
||||
),
|
||||
"freeze_frequency",
|
||||
).useWrappingLabel(),
|
||||
|
||||
CheckBoxGroupBlock(
|
||||
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.label"),
|
||||
listOf(
|
||||
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.none"), "no_freezes"),
|
||||
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.opening"), "opening_project"),
|
||||
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.indexing"), "indexing"),
|
||||
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.running"), "running_debugging"),
|
||||
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.terminal"), "terminal"),
|
||||
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.vcs"), "vcs"),
|
||||
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.docker"), "docker"),
|
||||
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.editing"), "editing"),
|
||||
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.branches"), "switching_branches"),
|
||||
CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.search"), "search_navigation"),
|
||||
),
|
||||
"freeze_situations",
|
||||
).setColumnCount(2).addOtherTextField(),
|
||||
|
||||
TextAreaBlock(
|
||||
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.additional.label", ideName),
|
||||
"additional_feedback",
|
||||
),
|
||||
)
|
||||
|
||||
init {
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val centerPanel = super.createCenterPanel()
|
||||
// The survey is long; cap the default height at 80% of the target screen (per the platform window-size
|
||||
// guidelines) and let the content scroll, instead of growing the dialog to the full content height.
|
||||
val maxHeight = (targetScreenBounds().height * 0.8).toInt()
|
||||
val preferred = centerPanel.preferredSize
|
||||
if (preferred.height > maxHeight) {
|
||||
centerPanel.preferredSize = Dimension(preferred.width, maxHeight)
|
||||
}
|
||||
return centerPanel
|
||||
}
|
||||
|
||||
/** Bounds of the screen the dialog will appear on (it is centered over the parent project frame). */
|
||||
private fun targetScreenBounds(): Rectangle {
|
||||
val frame = myProject?.let { WindowManager.getInstance().getFrame(it) }
|
||||
return if (frame != null) ScreenUtil.getScreenRectangle(frame) else ScreenUtil.getMainScreenBounds()
|
||||
}
|
||||
|
||||
override fun showThanksNotification() {
|
||||
ThanksForFeedbackNotification(
|
||||
description = WslSatisfactionFeedbackBundle.message("wsl.satisfaction.thanks.feedback.content")
|
||||
).notify(myProject)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.platform.feedback.wsl
|
||||
|
||||
import com.intellij.platform.feedback.FeedbackSurvey
|
||||
import com.intellij.platform.feedback.FeedbackSurveyType
|
||||
import com.intellij.platform.feedback.InIdeFeedbackSurveyConfig
|
||||
import com.intellij.platform.feedback.InIdeFeedbackSurveyType
|
||||
|
||||
internal class WslSatisfactionSurvey : FeedbackSurvey() {
|
||||
override val feedbackSurveyType: FeedbackSurveyType<InIdeFeedbackSurveyConfig> =
|
||||
InIdeFeedbackSurveyType(WslSatisfactionSurveyConfig())
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.platform.feedback.wsl
|
||||
|
||||
import com.intellij.openapi.application.ApplicationInfo
|
||||
import com.intellij.openapi.application.InitialConfigImportState
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.platform.feedback.InIdeFeedbackSurveyConfig
|
||||
import com.intellij.platform.feedback.dialog.BlockBasedFeedbackDialog
|
||||
import com.intellij.platform.feedback.dialog.SystemDataJsonSerializable
|
||||
import com.intellij.platform.feedback.impl.notification.RequestFeedbackNotification
|
||||
import com.intellij.util.PlatformUtils
|
||||
import com.intellij.util.system.LowLevelLocalMachineAccess
|
||||
import com.intellij.util.system.OS
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
internal class WslSatisfactionSurveyConfig : InIdeFeedbackSurveyConfig {
|
||||
|
||||
override val surveyId: String = "wsl_satisfaction"
|
||||
|
||||
// Survey is targeted at 2026.2 + 2026.2.x; collect for ~two months after release. Adjust to the actual release date.
|
||||
override val lastDayOfFeedbackCollection: LocalDate = LocalDate(2026, 10, 1)
|
||||
|
||||
override val requireIdeEAP: Boolean = false
|
||||
|
||||
@OptIn(LowLevelLocalMachineAccess::class)
|
||||
override fun checkIdeIsSuitable(): Boolean {
|
||||
// WSL exists only on Windows; the brief limits the survey to these IDEs.
|
||||
return OS.CURRENT == OS.Windows &&
|
||||
(PlatformUtils.isIdeaUltimate() ||
|
||||
PlatformUtils.isIdeaCommunity() ||
|
||||
PlatformUtils.isPyCharm() ||
|
||||
PlatformUtils.isWebStorm() ||
|
||||
PlatformUtils.isPhpStorm() ||
|
||||
PlatformUtils.isGoIde())
|
||||
}
|
||||
|
||||
override fun checkExtraConditionSatisfied(project: Project): Boolean {
|
||||
if (!isTargetVersion()) return false
|
||||
// Don't show for new users - only for existing users who upgraded to the new version.
|
||||
if (InitialConfigImportState.isNewUser()) return false
|
||||
if (!project.isWslProject()) return false
|
||||
return WslSatisfactionSurveyStore.getInstance().shouldShowDialog()
|
||||
}
|
||||
|
||||
private fun isTargetVersion(): Boolean {
|
||||
return ApplicationInfo.getInstance().shortVersion == TARGET_VERSION
|
||||
}
|
||||
|
||||
override fun createFeedbackDialog(project: Project, forTest: Boolean): BlockBasedFeedbackDialog<out SystemDataJsonSerializable> {
|
||||
return WslSatisfactionFeedbackDialog(project, forTest)
|
||||
}
|
||||
|
||||
override fun updateStateAfterDialogClosedOk(project: Project) {
|
||||
WslSatisfactionSurveyStore.getInstance().recordSurveyShown()
|
||||
}
|
||||
|
||||
override fun createNotification(project: Project, forTest: Boolean): RequestFeedbackNotification {
|
||||
return RequestFeedbackNotification(
|
||||
"Feedback In IDE",
|
||||
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.notification.request.title"),
|
||||
WslSatisfactionFeedbackBundle.message("wsl.satisfaction.notification.request.content"),
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateStateAfterNotificationShowed(project: Project) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TARGET_VERSION: String = "2026.2"
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.platform.feedback.wsl
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.startup.ProjectActivity
|
||||
|
||||
/**
|
||||
* Records that a WSL project was opened, feeding the "days working with WSL" counter used by
|
||||
* [WslSatisfactionSurveyConfig] to decide when to show the WSL satisfaction survey.
|
||||
*/
|
||||
internal class WslSatisfactionSurveyProjectActivity : ProjectActivity {
|
||||
override suspend fun execute(project: Project) {
|
||||
if (!project.isWslProject()) return
|
||||
WslSatisfactionSurveyStore.getInstance().recordWslProjectOpened()
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.platform.feedback.wsl
|
||||
|
||||
import com.intellij.openapi.components.BaseState
|
||||
import com.intellij.openapi.components.PersistentStateComponent
|
||||
import com.intellij.openapi.components.RoamingType
|
||||
import com.intellij.openapi.components.State
|
||||
import com.intellij.openapi.components.Storage
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.guessProjectDir
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.util.system.LowLevelLocalMachineAccess
|
||||
import com.intellij.util.system.OS
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
private val WSL_PATH_PREFIXES = listOf("//wsl$/", "//wsl.localhost/")
|
||||
|
||||
/**
|
||||
* Whether [this] project is opened from a WSL file system.
|
||||
*
|
||||
* Uses the same lightweight base-path prefix check as
|
||||
* [com.intellij.featureStatistics.fusCollectors.WslUsagesCollector], so it can be called
|
||||
* on any thread without touching the (potentially slow) distribution manager.
|
||||
*/
|
||||
@OptIn(LowLevelLocalMachineAccess::class)
|
||||
internal fun Project.isWslProject(): Boolean {
|
||||
if (OS.CURRENT != OS.Windows) return false
|
||||
val projectPath = guessProjectDir()?.path ?: return false
|
||||
return WSL_PATH_PREFIXES.any { projectPath.startsWith(it) }
|
||||
}
|
||||
|
||||
internal class WslSatisfactionSurveyState : BaseState() {
|
||||
// Unix time seconds of the first detected WSL project open.
|
||||
var firstWslUseTime: Long by property(0L)
|
||||
|
||||
// Number of distinct days on which a WSL project was open.
|
||||
var daysWithWslProject: Int by property(0)
|
||||
|
||||
// Unix time seconds; [daysWithWslProject] is incremented at most once after this moment.
|
||||
var nextCountedDay: Long by property(0L)
|
||||
var userSawSurvey: Boolean by property(false)
|
||||
}
|
||||
|
||||
@State(name = "WslSatisfactionSurveyStore", storages = [Storage(value = "wsl-survey.xml", roamingType = RoamingType.DISABLED)])
|
||||
internal class WslSatisfactionSurveyStore : PersistentStateComponent<WslSatisfactionSurveyState> {
|
||||
|
||||
internal var currentState: WslSatisfactionSurveyState = WslSatisfactionSurveyState()
|
||||
|
||||
override fun getState(): WslSatisfactionSurveyState = currentState
|
||||
|
||||
override fun loadState(state: WslSatisfactionSurveyState) {
|
||||
currentState = state
|
||||
}
|
||||
|
||||
internal fun recordSurveyShown() {
|
||||
currentState.userSawSurvey = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Records that a WSL project was open now. Increments [WslSatisfactionSurveyState.daysWithWslProject]
|
||||
* at most once per calendar day and stamps the first-usage time.
|
||||
*/
|
||||
internal fun recordWslProjectOpened() {
|
||||
val now = Instant.now()
|
||||
if (now >= Instant.ofEpochSecond(currentState.nextCountedDay)) {
|
||||
currentState.daysWithWslProject++
|
||||
currentState.nextCountedDay = (now + Duration.ofDays(1)).epochSecond
|
||||
}
|
||||
if (currentState.firstWslUseTime == 0L) {
|
||||
currentState.firstWslUseTime = now.epochSecond
|
||||
}
|
||||
}
|
||||
|
||||
internal fun shouldShowDialog(): Boolean {
|
||||
val state = currentState
|
||||
if (state.userSawSurvey) return false
|
||||
|
||||
val daysWithWslProject = Registry.intValue(REGISTRY_DAYS_WITH_WSL, -1).takeIf { it >= 0 } ?: state.daysWithWslProject
|
||||
if (daysWithWslProject < MINIMUM_DAYS_WITH_WSL_PROJECT) return false
|
||||
|
||||
val firstWslUseTime = firstWslUseTime() ?: return false
|
||||
return Duration.between(firstWslUseTime, Instant.now()) >= MINIMUM_DURATION_SINCE_FIRST_USE
|
||||
}
|
||||
|
||||
private fun firstWslUseTime(): Instant? {
|
||||
val override = Registry.stringValue(REGISTRY_FIRST_USE_DATE)
|
||||
if (override.isNotBlank()) {
|
||||
return runCatching { java.time.LocalDate.parse(override).atStartOfDay(java.time.ZoneOffset.UTC).toInstant() }.getOrNull()
|
||||
}
|
||||
if (currentState.firstWslUseTime == 0L) return null
|
||||
return Instant.ofEpochSecond(currentState.firstWslUseTime)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// "Project opened in WSL x3" + "after 3-5 days working with WSL" (IJPL-246739 brief).
|
||||
internal const val MINIMUM_DAYS_WITH_WSL_PROJECT: Int = 3
|
||||
internal val MINIMUM_DURATION_SINCE_FIRST_USE: Duration = Duration.ofDays(3)
|
||||
|
||||
internal const val REGISTRY_FIRST_USE_DATE: String = "wsl.survey.first.use.date"
|
||||
internal const val REGISTRY_DAYS_WITH_WSL: String = "wsl.survey.days.with.wsl"
|
||||
|
||||
fun getInstance(): WslSatisfactionSurveyStore = service()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.platform.feedback.wsl
|
||||
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.platform.feedback.dialog.CommonFeedbackSystemData
|
||||
import com.intellij.platform.feedback.dialog.SystemDataJsonSerializable
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
|
||||
/**
|
||||
* Data attached automatically to the WSL satisfaction survey feedback, in addition to the answers:
|
||||
* IDE name, WSL distribution and version, and whether WSL commands run via the IJent/EEL API.
|
||||
*/
|
||||
@Serializable
|
||||
internal data class WslSatisfactionSystemData(
|
||||
@NlsSafe val ideName: String,
|
||||
@NlsSafe val wslDistribution: String,
|
||||
val wslVersion: Int,
|
||||
val isEelApiUsed: Boolean,
|
||||
val commonData: CommonFeedbackSystemData,
|
||||
) : SystemDataJsonSerializable {
|
||||
override fun serializeToJson(json: Json): JsonElement = json.encodeToJsonElement(this)
|
||||
}
|
||||
Reference in New Issue
Block a user