Angular/angular-backend/src/org/angular2/cli/actions/AngularCliAddDependencyAction.kt (379 lines of code) (raw):

// 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.angular2.cli.actions import com.intellij.codeInsight.completion.CompletionParameters import com.intellij.codeInsight.completion.CompletionResultSet import com.intellij.codeInsight.lookup.CharFilter import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.execution.ExecutionException import com.intellij.execution.filters.Filter import com.intellij.execution.process.ProcessAdapter import com.intellij.execution.process.ProcessEvent import com.intellij.icons.AllIcons import com.intellij.javascript.nodejs.CompletionModuleInfo import com.intellij.javascript.nodejs.NodeModuleSearchUtil import com.intellij.javascript.nodejs.PackageJsonData import com.intellij.javascript.nodejs.interpreter.NodeJsInterpreterManager import com.intellij.javascript.nodejs.npm.registry.NpmRegistryService import com.intellij.javascript.nodejs.packageJson.NodeInstalledPackageFinder import com.intellij.javascript.nodejs.packageJson.NodePackageBasicInfo import com.intellij.javascript.nodejs.util.NodePackage import com.intellij.lang.javascript.boilerplate.NpmPackageProjectGenerator import com.intellij.lang.javascript.buildTools.npm.PackageJsonUtil 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.diagnostic.Logger import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.LabeledComponent import com.intellij.openapi.ui.Messages import com.intellij.openapi.ui.popup.IconButton import com.intellij.openapi.ui.popup.JBPopup import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.util.Ref import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VirtualFile import com.intellij.ui.* import com.intellij.ui.components.JBList import com.intellij.ui.scale.JBUIScale import com.intellij.ui.speedSearch.ListWithFilter import com.intellij.util.gist.GistManager import com.intellij.util.ui.EmptyIcon import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import org.angular2.cli.AngularCliFilter import org.angular2.cli.AngularCliProjectGenerator import org.angular2.cli.AngularCliSchematicsRegistryService import org.angular2.cli.AngularCliUtil import org.angular2.lang.Angular2Bundle import org.angular2.lang.Angular2LangUtil.ANGULAR_CLI_PACKAGE import org.jetbrains.annotations.NonNls import java.awt.BorderLayout import java.awt.Component import java.awt.Dimension import java.awt.event.KeyAdapter import java.awt.event.KeyEvent import java.awt.event.MouseEvent import java.io.IOException import javax.swing.Action import javax.swing.JComponent import javax.swing.JList import javax.swing.JPanel class AngularCliAddDependencyAction : DumbAwareAction() { override fun actionPerformed(e: AnActionEvent) { val project = e.project val file = e.getData(CommonDataKeys.VIRTUAL_FILE) if (project == null || file == null) { return } val cli = AngularCliUtil.findAngularCliFolder(project, file) val packageJson = PackageJsonUtil.findChildPackageJsonFile(cli) if (cli == null || packageJson == null) { return } if (!AngularCliUtil.hasAngularCLIPackageInstalled(cli)) { AngularCliUtil.notifyAngularCliNotInstalled( project, cli, Angular2Bundle.message("angular.action.ng-add.cant-add-new-dependency")) return } val existingPackages = PackageJsonData.getOrCreate(packageJson).allDependencies val model = SortedListModel( Comparator.comparing { p: NodePackageBasicInfo -> if (p === OTHER) 1 else 0 } .thenComparing<String> { it.name } ) val list = JBList(model) list.setCellRenderer(object : ColoredListCellRenderer<NodePackageBasicInfo>() { override fun customizeCellRenderer( list: JList<out NodePackageBasicInfo>, value: NodePackageBasicInfo, index: Int, selected: Boolean, hasFocus: Boolean, ) { if (!selected && index % 2 == 0) { background = UIUtil.getDecoratedRowColor() } setIcon(JBUIScale.scaleIcon(EmptyIcon.create(5))) append(value.name, if (value !== OTHER) SimpleTextAttributes.REGULAR_ATTRIBUTES else SimpleTextAttributes.LINK_ATTRIBUTES, true) if (value.description != null) { append(" - " + value.description!!, SimpleTextAttributes.GRAY_ATTRIBUTES, false) } } }) val scroll = ScrollPaneFactory.createScrollPane(list) scroll.border = JBUI.Borders.empty() val pane = ListWithFilter.wrap(list, scroll) { it.name } @Suppress("DialogTitleCapitalization") val builder = JBPopupFactory .getInstance() .createComponentPopupBuilder(pane, list) .setMayBeParent(true) .setRequestFocus(true) .setFocusable(true) .setFocusOwners(arrayOf<Component>(list)) .setLocateWithinScreenBounds(true) .setCancelOnOtherWindowOpen(true) .setMovable(true) .setResizable(true) .setCancelOnWindowDeactivation(false) .setTitle(Angular2Bundle.message("angular.action.ng-add.title")) .setCancelOnClickOutside(true) .setDimensionServiceKey(project, "org.angular.cli.generate", true) .setMinSize(Dimension(JBUIScale.scale(350), JBUIScale.scale(300))) .setCancelButton(IconButton(Angular2Bundle.message("angular.action.ng-add.button-close"), AllIcons.Actions.Close, AllIcons.Actions.CloseHovered)) val popup = builder.createPopup() val action = { pkgInfo: NodePackageBasicInfo -> popup.closeOk(null) if (pkgInfo == OTHER) { chooseCustomPackageAndInstall(project, cli, existingPackages) } else { runAndShowConsole(project, cli, pkgInfo.name, false) } } list.addKeyListener(object : KeyAdapter() { override fun keyPressed(e: KeyEvent) { if (list.selectedValue == null) return if (e.keyCode == KeyEvent.VK_ENTER) { e.consume() action(list.selectedValue) } } }) object : DoubleClickListener() { public override fun onDoubleClick(event: MouseEvent): Boolean { if (list.selectedValue == null) return true action(list.selectedValue) return true } }.installOn(list) popup.showCenteredInCurrentWindow(project) updateListAsync(project, list, model, popup, existingPackages) } override fun update(e: AnActionEvent) { val project = e.project val file = e.getData(CommonDataKeys.VIRTUAL_FILE) e.presentation.isEnabledAndVisible = (project != null && file != null && AngularCliUtil.findAngularCliFolder(project, file) != null) } override fun getActionUpdateThread(): ActionUpdateThread { return ActionUpdateThread.BGT } private class SelectCustomPackageDialog( private val myProject: Project, private val myExistingPackages: Set<String>, private val contextFile: VirtualFile, ) : DialogWrapper(myProject) { private var myTextEditor: EditorTextField? = null val `package`: String get() = myTextEditor!!.text init { @Suppress("DialogTitleCapitalization") title = Angular2Bundle.message("angular.action.ng-add.title") init() okAction.putValue(Action.NAME, Angular2Bundle.message("angular.action.ng-add.button-install")) } override fun getPreferredFocusedComponent(): JComponent? { return myTextEditor } override fun createCenterPanel(): JComponent { val panel = JPanel(BorderLayout(0, 4)) myTextEditor = TextFieldWithAutoCompletion( myProject, NodePackagesCompletionProvider(myProject, myExistingPackages, contextFile), false, null) myTextEditor!!.setPreferredWidth(250) panel.add(LabeledComponent.create(myTextEditor!!, Angular2Bundle.message("angular.action.ng-add.package-name"), BorderLayout.NORTH)) return panel } } private class NodePackagesCompletionProvider( private val project: Project, private val myExistingPackages: Set<String>, private val contextFile: VirtualFile, ) : TextFieldWithAutoCompletionListProvider<NodePackageBasicInfo>( emptyList()) { override fun getLookupString(item: NodePackageBasicInfo): String { return item.name } override fun acceptChar(c: Char): CharFilter.Result? { return if (c == '@' || c == '/') CharFilter.Result.ADD_TO_PREFIX else null } override fun applyPrefixMatcher(result: CompletionResultSet, prefix: String): CompletionResultSet { val res = super.applyPrefixMatcher(result, prefix) res.restartCompletionOnAnyPrefixChange() return res } override fun getItems(prefix: String, cached: Boolean, parameters: CompletionParameters): Collection<NodePackageBasicInfo> { if (cached) { return emptyList() } val result = ArrayList<NodePackageBasicInfo>() try { NpmRegistryService.getInstance(project).findPackages( NpmRegistryService.namePrefixSearch(prefix), 20, contextFile, { true }, { pkg -> if (!myExistingPackages.contains(pkg.name)) { result.add(pkg) } }) } catch (e: IOException) { LOG.info(e) } return result } override fun createLookupBuilder(item: NodePackageBasicInfo): LookupElementBuilder { return super.createLookupBuilder(item) .withTailText(if (item.description != null) " " + item.description!! else null, true) } } companion object { private val OTHER = NodePackageBasicInfo(Angular2Bundle.message("angular.action.ng-add.install-other"), null) @NonNls private val LOG = Logger.getInstance(AngularCliAddDependencyAction::class.java) private const val TIMEOUT: Long = 2000 @NonNls private val LATEST = "latest" @JvmStatic fun runAndShowConsoleLater( project: Project, cli: VirtualFile, packageName: String, packageVersion: String?, proposeLatestVersionIfNeeded: Boolean, ) { ApplicationManager.getApplication().executeOnPooledThread { if (project.isDisposed) { return@executeOnPooledThread } val version = Ref(StringUtil.defaultIfEmpty(packageVersion, LATEST)) val proposeLatestVersion = proposeLatestVersionIfNeeded && !AngularCliSchematicsRegistryService.getInstance(project).supportsNgAdd(packageName, version.get(), TIMEOUT) ApplicationManager.getApplication().invokeLater( { if (proposeLatestVersion) { @Suppress("DialogTitleCapitalization") when (Messages.showDialog( project, Angular2Bundle.message("angular.action.ng-add.not-supported-specified-try-latest"), Angular2Bundle.message("angular.action.ng-add.title"), arrayOf(Angular2Bundle.message("angular.action.ng-add.install-latest"), Angular2Bundle.message("angular.action.ng-add.install-current"), Messages.getCancelButton()), 0, Messages.getQuestionIcon())) { 0 -> version.set(LATEST) 1 -> version.set(packageVersion) else -> return@invokeLater } } runAndShowConsole(project, cli, packageName + "@" + version.get(), !proposeLatestVersion) }, project.disposed) } } private fun runAndShowConsole( project: Project, cli: VirtualFile, packageSpec: String, proposeLatestVersionIfNeeded: Boolean, ) { if (project.isDisposed) { return } val interpreter = NodeJsInterpreterManager.getInstance(project).interpreter ?: return try { val modules = ArrayList<CompletionModuleInfo>() NodeModuleSearchUtil.findModulesWithName(modules, ANGULAR_CLI_PACKAGE, cli, null) if (modules.isEmpty() || modules[0].virtualFile == null) { throw ExecutionException(Angular2Bundle.message("angular.action.ng-add.pacakge-not-installed")) } val module = modules[0] ApplicationManager.getApplication().executeOnPooledThread { val handler = NpmPackageProjectGenerator.generate( interpreter, NodePackage(module.virtualFile!!.path), { pkg -> pkg.findBinFilePath(AngularCliProjectGenerator.NG_EXECUTABLE)!!.toString() }, cli, VfsUtilCore.virtualToIoFile(cli), project, { GistManager.getInstance().invalidateData() }, Angular2Bundle.message("angular.action.ng-add.installing-for", packageSpec, cli.name), arrayOf<Filter>(AngularCliFilter(project, cli.path)), "add", packageSpec) if (proposeLatestVersionIfNeeded) { handler.addProcessListener(object : ProcessAdapter() { override fun processTerminated(event: ProcessEvent) { if (event.exitCode != 0) { installLatestIfFeasible(project, cli, packageSpec) } } }) } } } catch (e: Exception) { LOG.error("Failed to execute `ng add`: " + e.message, e) } } private fun installLatestIfFeasible( project: Project, cli: VirtualFile, packageSpec: String, ) { if (project.isDisposed) { return } val packageJson = PackageJsonUtil.findChildPackageJsonFile(cli) ?: return val finder = NodeInstalledPackageFinder(project, packageJson) val index = packageSpec.lastIndexOf('@') val packageName = if (index <= 0) packageSpec else packageSpec.substring(0, index) val pkg = finder.findInstalledPackage(packageName) ?: return if (!AngularCliSchematicsRegistryService.getInstance(project).supportsNgAdd(pkg)) { ApplicationManager.getApplication().invokeLater( { @Suppress("DialogTitleCapitalization") if (Messages.OK == Messages.showDialog( project, Angular2Bundle.message("angular.action.ng-add.not-supported-installed-try-latest"), Angular2Bundle.message("angular.action.ng-add.title"), arrayOf(Angular2Bundle.message("angular.action.ng-add.install-latest"), Messages.getCancelButton()), 0, Messages.getQuestionIcon())) { runAndShowConsole(project, cli, "$packageName@$LATEST", false) } }, project.disposed ) } } private fun chooseCustomPackageAndInstall( project: Project, cli: VirtualFile, existingPackages: Set<String>, ) { val dialog = SelectCustomPackageDialog(project, existingPackages, cli) if (dialog.showAndGet()) { runAndShowConsole(project, cli, dialog.`package`, false) } } private fun updateListAsync( project: Project, list: JBList<NodePackageBasicInfo>, model: SortedListModel<NodePackageBasicInfo>, popup: JBPopup, existingPackages: Set<String>, ) { list.setPaintBusy(true) model.clear() ApplicationManager.getApplication().executeOnPooledThread { if (popup.isDisposed) { return@executeOnPooledThread } val packages = AngularCliSchematicsRegistryService .getInstance(project) .getPackagesSupportingNgAdd(20000) ApplicationManager.getApplication().invokeLater( { packages.forEach { pkg -> if (!existingPackages.contains(pkg.name)) { model.add(pkg) } } model.add(OTHER) list.setPaintBusy(false) }, { popup.isDisposed }) } } } }