diff --git a/BUILD.bazel b/BUILD.bazel index 0b44e2cedc99..a293175b3e92 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -364,6 +364,8 @@ jvm_library( "//plugins/jsonpath", "//plugins/jsonpath:jsonpath_test_lib", "//plugins/ui-designer/jps-plugin/tests:tests_test_lib", + "//platform/non-modal-welcome-screen", + "//platform/non-modal-welcome-screen:non-modal-welcome-screen_test_lib", "//platform/util/coroutines:coroutines-tests_test_lib", "//platform/util/progress:progress-tests_test_lib", "//platform/testFramework/junit5.jimfs", diff --git a/intellij.idea.community.main.iml b/intellij.idea.community.main.iml index de7598f76638..52403e4b0582 100644 --- a/intellij.idea.community.main.iml +++ b/intellij.idea.community.main.iml @@ -217,6 +217,7 @@ + diff --git a/platform/non-modal-welcome-screen/BUILD.bazel b/platform/non-modal-welcome-screen/BUILD.bazel index fc9580456521..f0e85073a2ef 100644 --- a/platform/non-modal-welcome-screen/BUILD.bazel +++ b/platform/non-modal-welcome-screen/BUILD.bazel @@ -36,4 +36,47 @@ jvm_library( ], plugins = ["@lib//:compose-plugin"] ) -### auto-generated section `build intellij.platform.ide.nonModalWelcomeScreen` end \ No newline at end of file + +jvm_library( + name = "non-modal-welcome-screen_test_lib", + visibility = ["//visibility:public"], + srcs = glob(["testSrc/**/*.kt", "testSrc/**/*.java", "testSrc/**/*.form"], allow_empty = True), + associates = [":non-modal-welcome-screen"], + deps = [ + "//platform/analysis-api:analysis", + "//platform/editor-ui-api:editor-ui", + "//platform/compose", + "//platform/compose:compose_test_lib", + "//platform/core-api:core", + "//jps/model-api:model", + "//platform/lang-core", + "//libraries/compose-runtime-desktop", + "//platform/projectModel-api:projectModel", + "//platform/testFramework", + "//platform/testFramework:testFramework_test_lib", + "//platform/util:util-ui", + "//platform/platform-api:ide", + "//platform/lang-impl", + "//platform/core-ui", + "//platform/ide-core-impl", + "//platform/platform-impl:ide-impl", + "@lib//:junit5", + "//platform/statistics", + "//platform/statistics:statistics_test_lib", + "//libraries/kotlinx/serialization/core", + "//platform/kernel/shared:kernel", + "//platform/platform-impl/rpc", + "//platform/project/shared:project", + ], + plugins = ["@lib//:compose-plugin"] +) +### auto-generated section `build intellij.platform.ide.nonModalWelcomeScreen` end + +### auto-generated section `test intellij.platform.ide.nonModalWelcomeScreen` start +load("@community//build:tests-options.bzl", "jps_test") + +jps_test( + name = "non-modal-welcome-screen_test", + runtime_deps = [":non-modal-welcome-screen_test_lib"] +) +### auto-generated section `test intellij.platform.ide.nonModalWelcomeScreen` end \ No newline at end of file diff --git a/platform/non-modal-welcome-screen/intellij.platform.ide.nonModalWelcomeScreen.iml b/platform/non-modal-welcome-screen/intellij.platform.ide.nonModalWelcomeScreen.iml index 2178cd6dd828..2252ce1bf20b 100644 --- a/platform/non-modal-welcome-screen/intellij.platform.ide.nonModalWelcomeScreen.iml +++ b/platform/non-modal-welcome-screen/intellij.platform.ide.nonModalWelcomeScreen.iml @@ -30,6 +30,7 @@ + diff --git a/platform/non-modal-welcome-screen/src/com/intellij/platform/ide/nonModalWelcomeScreen/newFileDialog/WelcomeScreenNewFileDialog.kt b/platform/non-modal-welcome-screen/src/com/intellij/platform/ide/nonModalWelcomeScreen/newFileDialog/WelcomeScreenNewFileDialog.kt index affbe6154d0b..af805407ded5 100644 --- a/platform/non-modal-welcome-screen/src/com/intellij/platform/ide/nonModalWelcomeScreen/newFileDialog/WelcomeScreenNewFileDialog.kt +++ b/platform/non-modal-welcome-screen/src/com/intellij/platform/ide/nonModalWelcomeScreen/newFileDialog/WelcomeScreenNewFileDialog.kt @@ -22,10 +22,12 @@ import com.intellij.util.ui.FormBuilder import org.jetbrains.annotations.ApiStatus import java.awt.event.FocusAdapter import java.awt.event.FocusEvent +import java.nio.file.InvalidPathException import java.nio.file.Path import javax.swing.JComponent import javax.swing.JList import javax.swing.event.DocumentEvent +import kotlin.io.path.invariantSeparatorsPathString @ApiStatus.Internal class WelcomeScreenNewFileDialog private constructor( @@ -33,8 +35,24 @@ class WelcomeScreenNewFileDialog private constructor( private val builder: Builder, ) : DialogWrapper(project, true) { - private companion object { + internal companion object { private const val MAX_PATH_LENGTH = 70 + + /** + * Normalizes a directory path for use with [DirectoryUtil.mkdirs]. + * + * This method ensures the path: + * - Uses forward slashes (/) as separators, which is required by [DirectoryUtil.mkdirs] + * - Has redundant path elements (like `.` and `..`) resolved + * + * This is necessary because on Windows, [Path.toString] returns paths with backslashes, + * but [DirectoryUtil.mkdirs] requires forward slashes. + * + * @see IJPL-217109 + */ + fun normalizeDirectoryPath(path: String): String { + return Path.of(path).normalize().invariantSeparatorsPathString + } } private val targetDirectoryField: ComponentWithBrowseButton = ComponentWithBrowseButton(ExtendableTextField(), null) @@ -155,7 +173,7 @@ class WelcomeScreenNewFileDialog private constructor( val targetDirectoryName = targetDirectoryField.childComponent.text - if (targetDirectoryName.isEmpty()) { + if (targetDirectoryName.isNullOrEmpty()) { Messages.showErrorDialog( project, NonModalWelcomeScreenBundle.message("welcome.screen.create.file.dialog.no.target.directory.specified"), @@ -168,12 +186,14 @@ class WelcomeScreenNewFileDialog private constructor( ApplicationManager.getApplication().runWriteAction { try { targetDirectory = DirectoryUtil.mkdirs( - PsiManager.getInstance(project), - Path.of(targetDirectoryName).normalize().toString() + PsiManager.getInstance(project), + normalizeDirectoryPath(targetDirectoryName) ) } catch (_: IncorrectOperationException) { } + catch (_: InvalidPathException) { + } } }, NonModalWelcomeScreenBundle.message("welcome.screen.create.file.dialog.create.directory"), null) diff --git a/platform/non-modal-welcome-screen/testSrc/com/intellij/platform/ide/nonModalWelcomeScreen/newFileDialog/WelcomeScreenNewFileDialogTest.kt b/platform/non-modal-welcome-screen/testSrc/com/intellij/platform/ide/nonModalWelcomeScreen/newFileDialog/WelcomeScreenNewFileDialogTest.kt new file mode 100644 index 000000000000..0cfae0c357f8 --- /dev/null +++ b/platform/non-modal-welcome-screen/testSrc/com/intellij/platform/ide/nonModalWelcomeScreen/newFileDialog/WelcomeScreenNewFileDialogTest.kt @@ -0,0 +1,99 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.ide.nonModalWelcomeScreen.newFileDialog + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.EnabledOnOs +import org.junit.jupiter.api.condition.OS +import java.nio.file.FileSystems + +/** + * Tests for [WelcomeScreenNewFileDialog.normalizeDirectoryPath]. + * + * The method ensures paths use forward slashes, which is required by [DirectoryUtil.mkdirs]. + * + * **Note:** Windows-specific tests use [@EnabledOnOs] and will be skipped on other platforms. + * They will run on Windows agents in TeamCity. + * + * @see IJPL-217109 + */ +class WelcomeScreenNewFileDialogTest { + + /** + * Regression test for IJPL-217109: [DirectoryUtil.mkdirs] requires forward slashes. + * Simulates user input with platform-native separators (backslashes on Windows). + */ + @Test + fun `normalizeDirectoryPath uses forward slashes`() { + val separator = FileSystems.getDefault().separator + val userInput = listOf("Users", "test", "NewProject").joinToString(separator) + + val normalized = WelcomeScreenNewFileDialog.normalizeDirectoryPath(userInput) + + assertFalse(normalized.contains("\\"), + "Path for DirectoryUtil must not contain backslashes: $normalized") + } + + @Test + fun `normalizeDirectoryPath resolves parent directory references`() { + val pathWithDots = "home/user/../user/project" + + val normalized = WelcomeScreenNewFileDialog.normalizeDirectoryPath(pathWithDots) + + assertFalse(normalized.contains(".."), "Normalized path should not contain '..': $normalized") + assertEquals("home/user/project", normalized) + } + + @Test + fun `normalizeDirectoryPath resolves current directory references`() { + val pathWithDot = "home/./user/./project" + + val normalized = WelcomeScreenNewFileDialog.normalizeDirectoryPath(pathWithDot) + + assertEquals("home/user/project", normalized) + } + + /** + * Regression test for IJPL-217109: Windows backslash paths must be converted to forward slashes. + * This test runs ONLY on Windows agents in TeamCity. + */ + @Test + @EnabledOnOs(OS.WINDOWS) + fun `normalizeDirectoryPath converts Windows backslashes to forward slashes`() { + val windowsPath = "C:\\Users\\test\\project" + + val normalized = WelcomeScreenNewFileDialog.normalizeDirectoryPath(windowsPath) + + assertEquals("C:/Users/test/project", normalized) + } + + /** + * Tests Windows paths with mixed separators (common when copy-pasting paths). + * This test runs ONLY on Windows agents in TeamCity. + */ + @Test + @EnabledOnOs(OS.WINDOWS) + fun `normalizeDirectoryPath handles Windows mixed separators`() { + val mixedPath = "C:/Users\\test/project\\src" + + val normalized = WelcomeScreenNewFileDialog.normalizeDirectoryPath(mixedPath) + + assertEquals("C:/Users/test/project/src", normalized) + assertFalse(normalized.contains("\\"), "Path should not contain backslashes") + } + + /** + * Tests that trailing backslashes are handled correctly on Windows. + * This test runs ONLY on Windows agents in TeamCity. + */ + @Test + @EnabledOnOs(OS.WINDOWS) + fun `normalizeDirectoryPath handles Windows trailing separators`() { + val pathWithTrailing = "C:\\Users\\test\\" + + val normalized = WelcomeScreenNewFileDialog.normalizeDirectoryPath(pathWithTrailing) + + assertEquals("C:/Users/test", normalized) + } +}