Skip to content

Commit

Permalink
vue: add experimental volar support to work around WEB-60063
Browse files Browse the repository at this point in the history
GitOrigin-RevId: c7188fa04bd6d955ffe6c7875a38bba13da3754c
  • Loading branch information
anstarovoyt authored and intellij-monorepo-bot committed Apr 24, 2023
1 parent 43e510c commit 481ca95
Show file tree
Hide file tree
Showing 9 changed files with 370 additions and 14 deletions.
2 changes: 2 additions & 0 deletions vuejs/intellij.vuejs.iml
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,7 @@
<orderEntry type="library" name="commons-lang" level="project" />
<orderEntry type="module" module-name="intellij.platform.webSymbols" />
<orderEntry type="module" module-name="intellij.postcss" />
<orderEntry type="module" module-name="intellij.platform.lsp" />
<orderEntry type="module" module-name="intellij.platform.ide.util.io" />
</component>
</module>
15 changes: 15 additions & 0 deletions vuejs/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@
<psi.fileReferenceHelper implementation="org.jetbrains.vuejs.libraries.nuxt.codeInsight.NuxtFileReferenceHelper"/>
<useScopeEnlarger implementation="org.jetbrains.vuejs.findUsages.VueUseScopeEnlarger"/>
<implicitUsageProvider implementation="org.jetbrains.vuejs.codeInsight.refs.VueImplicitUsageProvider"/>
<lsp.serverSupportProvider implementation="org.jetbrains.vuejs.lang.typescript.service.volar.VolarSupportProvider"/>

<!--suppress PluginXmlValidity -->
<quoteHandler className="com.intellij.codeInsight.editorActions.HtmlQuoteHandler" fileType="Vue.js"/>
<copyPastePostProcessor implementation="org.jetbrains.vuejs.codeInsight.imports.VueTemplateExpressionsCopyPasteProcessor"/>
Expand Down Expand Up @@ -114,6 +116,15 @@
<fileIndentOptionsProvider implementation="org.jetbrains.vuejs.lang.html.psi.formatter.VueFileIndentOptionsProvider"
id="vue" order="before html"/>

<projectConfigurable parentId="Settings.JavaScript"
instance="org.jetbrains.vuejs.options.VueConfigurable"
id="settings.vue"
bundle="messages.VueBundle"
key="vue.configurable.title"
nonDefaultProject="true"
/>

<projectService serviceImplementation="org.jetbrains.vuejs.options.VueSettings" />

<postStartupActivity implementation="org.jetbrains.vuejs.lang.html.psi.arrangement.VueArrangementSettingsMigration"/>

Expand Down Expand Up @@ -349,6 +360,10 @@
</actions>

<extensions defaultExtensionNs="JavaScript">
<TypeScriptAnnotatorCheckerProvider
order="first"
implementation="org.jetbrains.vuejs.lang.typescript.service.volar.VolarTypeScriptAnnotationProvider"/>

<elementScopeProvider implementation="org.jetbrains.vuejs.VueElementResolveScopeProvider"/>
<indexedFileTypeProvider implementation="org.jetbrains.vuejs.lang.html.VueIndexedFileTypeProvider"/>

Expand Down
16 changes: 16 additions & 0 deletions vuejs/resources/messages/VueBundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,20 @@ vue.progress.title.auto-importing-vue-components-on-paste=Auto-importing Vue com

vue.command.name.auto-import-external-symbols=Auto-Import External Symbols
vue.command.name.auto-import-vue-components=Auto-Import Vue Components
vue.configurable.title=Vue
vue.configurable.service.group=Vue Service
vue.configurable.service.auto=Automatically
vue.configurable.service.auto.help=Select this option to enable service integration based on the project TypeScript version. \
If you are using TypeScript 5 of later, "Volar" will be enabled, for earlier version direct integration with TypeScript service will be used.
vue.configurable.service.ts=TypeScript
vue.configurable.service.ts.help=Select this option to enable direct integration with TypeScript service for vue files. \
Because this integration doesn't work for TypeScript version 5.0.0 and later, in such cases internal IDE inspections will be used instead.
vue.configurable.service.volar=Volar
vue.configurable.service.volar.help=Select this option to use Volar integration for all vue files. \
Please note that the integration is experimental, so it can be unstable in some cases.
vue.configurable.service.disabled=Disabled
vue.configurable.service.disabled.help=Select this option to turn both TypeScript Service and Volar off.
volar.package.download=Downloading {0}...
volar.interpreter.error=Local or WSL Node.js interpreter not configured.
volar.executable.error=Volar language server is not found.

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.vuejs.lang.typescript.service

import com.intellij.javascript.nodejs.PackageJsonData
import com.intellij.lang.typescript.compiler.languageService.TypeScriptLanguageServiceUtil
import com.intellij.lang.typescript.compiler.languageService.TypeScriptServerState
import com.intellij.lang.typescript.library.TypeScriptServiceDirectoryWatcher
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.vuejs.context.isVueContext
import org.jetbrains.vuejs.lang.html.VueFileType
import org.jetbrains.vuejs.lang.typescript.service.volar.getVolarExecutableAndRefresh
import org.jetbrains.vuejs.options.VueServiceSettings
import org.jetbrains.vuejs.options.getVueSettings


fun isVueServiceContext(project: Project, context: VirtualFile): Boolean = context.fileType is VueFileType || isVueContext(context, project)

fun isTypeScriptServiceBefore5Context(project: Project): Boolean {
val path = getTypeScriptServiceDirectory(project)

val packageJson = TypeScriptServerState.getPackageJsonFromServicePath(path)
if (packageJson == null) return true
val version = PackageJsonData.getOrCreate(packageJson).version ?: return true
return version.major < 5;
}

fun getTypeScriptServiceDirectory(project: Project): String {
val watcher = TypeScriptServiceDirectoryWatcher.getService(project)
return watcher.cachedCustomServiceDirectory ?: watcher.calcServiceDirectoryAndRefresh()
}

fun isVueTypeScriptServiceEnabled(project: Project, context: VirtualFile): Boolean {
if (!isVueServiceContext(project, context)) return false

return when (getVueSettings(project).serviceType) {
VueServiceSettings.AUTO -> isTypeScriptServiceBefore5Context(project)
else -> false
}
}

fun isVolarEnabled(project: Project, context: VirtualFile): Boolean {
return isVolarFileTypeAcceptable(context) && isVolarEnabledByContextAndSettings(project, context) && getVolarExecutableAndRefresh(project) != null
}

fun isVolarFileTypeAcceptable(file: VirtualFile): Boolean {
if (!TypeScriptLanguageServiceUtil.IS_VALID_FILE_FOR_SERVICE.value(file)) return false

return file.fileType == VueFileType.INSTANCE || TypeScriptLanguageServiceUtil.ACCEPTABLE_TS_FILE.value(file)
}

fun isVolarEnabledByContextAndSettings(project: Project, context: VirtualFile): Boolean {
if (!isVueServiceContext(project, context)) return false

return when (getVueSettings(project).serviceType) {
VueServiceSettings.VOLAR -> true
VueServiceSettings.AUTO -> !isTypeScriptServiceBefore5Context(project)
else -> false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,20 +104,7 @@ class VueTypeScriptService(project: Project) : TypeScriptServerServiceImpl(proje
return !isVueServiceAvailableByContext(context)
}

private fun isVueServiceAvailableByContext(context: VirtualFile): Boolean {
if (context.fileType is VueFileType) return isServiceVersionAcceptable()

//other files
return isVueContext(context, myProject) && isServiceVersionAcceptable()
}

private fun isServiceVersionAcceptable(): Boolean {
val path = TypeScriptServiceDirectoryWatcher.getService(myProject).calcServiceDirectoryAndRefresh()
val packageJson = TypeScriptServerState.getPackageJsonFromServicePath(path)
if (packageJson == null) return true
val version = PackageJsonData.getOrCreate(packageJson).version ?: return true
return version.major < 5;
}
private fun isVueServiceAvailableByContext(context: VirtualFile): Boolean = isVueTypeScriptServiceEnabled(myProject, context)

override fun createProtocol(readyConsumer: Consumer<*>, tsServicePath: String): JSLanguageServiceProtocol {
return VueTypeScriptServiceProtocol(myProject, mySettings, readyConsumer, createEventConsumer(), tsServicePath)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.vuejs.lang.typescript.service.volar

import com.google.gson.JsonParser
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.concurrency.SensitiveProgressWrapper
import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.javascript.nodejs.interpreter.NodeCommandLineConfigurator
import com.intellij.javascript.nodejs.interpreter.NodeJsInterpreterManager
import com.intellij.javascript.nodejs.interpreter.local.NodeJsLocalInterpreter
import com.intellij.javascript.nodejs.interpreter.wsl.WslNodeInterpreter
import com.intellij.lang.javascript.JavaScriptBundle
import com.intellij.lang.javascript.library.typings.TypeScriptExternalDefinitionsRegistry
import com.intellij.lang.javascript.library.typings.TypeScriptPackageName
import com.intellij.lsp.api.LspServerDescriptor
import com.intellij.lsp.api.LspServerManager
import com.intellij.lsp.api.LspServerSupportProvider
import com.intellij.lsp.api.LspServerSupportProvider.LspServerStarter
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.progress.util.ProgressWrapper
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.concurrency.AppExecutorUtil
import com.intellij.util.text.SemVer
import org.jetbrains.vuejs.VueBundle
import org.jetbrains.vuejs.lang.typescript.service.getTypeScriptServiceDirectory
import org.jetbrains.vuejs.lang.typescript.service.isVolarEnabled
import org.jetbrains.vuejs.lang.typescript.service.isVolarFileTypeAcceptable
import org.jetbrains.vuejs.options.VueServiceSettings
import org.jetbrains.vuejs.options.getVueSettings
import java.io.File
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.function.BiFunction

private val volarVersion = SemVer.parseFromText("1.4.0")
private const val volarPackage = "@volar/vue-language-server"


class VolarSupportProvider : LspServerSupportProvider {
override fun fileOpened(project: Project, file: VirtualFile, serverStarter: LspServerStarter) {
getVueLspServerDescriptor(project, file)?.let { serverStarter.ensureServerStarted(it) }
}
}

fun getVueLspServerDescriptor(project: Project, file: VirtualFile): LspServerDescriptor? {
if (!isVolarEnabled(project, file)) return null
val projectDir = project.guessProjectDir() ?: return null
return VolarLspServerDescriptor(project, projectDir)
}

class VolarLspServerDescriptor(project: Project, vararg roots: VirtualFile) : LspServerDescriptor(project, "Vue", *roots) {

override fun isSupportedFile(file: VirtualFile): Boolean {
return isVolarEnabled(project, file)
}

override fun createCommandLine(): GeneralCommandLine {
val interpreter = NodeJsInterpreterManager.getInstance(project).interpreter
if (interpreter !is NodeJsLocalInterpreter && interpreter !is WslNodeInterpreter) {
throw ExecutionException(VueBundle.message("volar.interpreter.error"))
}
val volarExecutable = getVolarExecutable()
if (volarExecutable == null) {
throw ExecutionException(VueBundle.message("volar.executable.error"))
}

return GeneralCommandLine().apply {
withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE)
withCharset(Charsets.UTF_8)
addParameter(volarExecutable)
addParameter("--stdio")

NodeCommandLineConfigurator.find(interpreter).configure(this, NodeCommandLineConfigurator.defaultOptions(project))
}
}

override fun createInitializationOptions(): Any {
val tsPath = runReadAction { getTypeScriptServiceDirectory(project) }
return JsonParser.parseString("{'typescript': { 'tsdk': '${tsPath}' }}")
}
}

fun getVolarExecutable(): String? {
val packageName = createPackage()
val path = TypeScriptExternalDefinitionsRegistry.getExactModuleTypingsPath(packageName) ?: return null
return if (!File(path).isDirectory) null else "$path${FileUtil.toSystemDependentName("/bin/vue-language-server.js")}"
}

private fun createPackage() = TypeScriptPackageName(volarPackage, volarVersion)

fun getVolarExecutableAndRefresh(project: Project): String? {
val executable = getVolarExecutable()
if (executable != null) return executable
scheduleVolarDownloading(project)
return null
}

fun scheduleVolarDownloading(project: Project) {
object : Task.Backgroundable(project, VueBundle.message("volar.package.download", volarPackage), true,
ALWAYS_BACKGROUND) {
override fun run(indicator: ProgressIndicator) {
indicator.isIndeterminate = true
val future = downloadVolar(project, indicator)
future.handleAsync(BiFunction { volarPath, _ ->
if (volarPath != null) {
runReadAction {

updateVolarLsp(project, isVolarSettingEnabled(project))
}
}
})
}
}.queue()
}

fun isVolarSettingEnabled(project: Project): Boolean {
val vueSettings = getVueSettings(project)
return vueSettings.serviceType == VueServiceSettings.AUTO || vueSettings.serviceType == VueServiceSettings.VOLAR
}

fun downloadVolar(project: Project, indicator: ProgressIndicator): CompletableFuture<VirtualFile?> {
return CompletableFuture.supplyAsync(
{
val definitionsRegistry = TypeScriptExternalDefinitionsRegistry.instance
val installProgress: ProgressWrapper = SensitiveProgressWrapper(indicator)
val future = definitionsRegistry.installPackage(createPackage(), project, installProgress)
try {
future[2, TimeUnit.MINUTES]
}
catch (e: InterruptedException) {
throw RuntimeException(
JavaScriptBundle.message("npm.failed_to_install_package.title.message", volarPackage), e)
}
catch (e: java.util.concurrent.ExecutionException) {
throw RuntimeException(
JavaScriptBundle.message("npm.failed_to_install_package.title.message", volarPackage), e)
}
catch (e: TimeoutException) {
installProgress.cancel()
throw RuntimeException(
JavaScriptBundle.message("npm.failed_to_install_package.title.message", volarPackage), e)
}
}, AppExecutorUtil.getAppExecutorService())
}

fun updateVolarLsp(project: Project, enabled: Boolean) {
if (enabled) {
for (openFile in FileEditorManager.getInstance(project).openFiles) {
val lspServerDescriptor = getVueLspServerDescriptor(project, openFile)
if (lspServerDescriptor != null) {
val lspServerManager = LspServerManager.getInstance(project)
lspServerManager.ensureServerStarted(VolarSupportProvider::class.java, lspServerDescriptor)
DaemonCodeAnalyzer.getInstance(project).restart()
return
}
}

if (getVueSettings(project).serviceType == VueServiceSettings.VOLAR) return
}

//in all other cases disable volar
val lspServerManager = LspServerManager.getInstance(project)
lspServerManager.getServersForProvider(VolarSupportProvider::class.java).forEach { lspServerManager.stopServer(it) }
DaemonCodeAnalyzer.getInstance(project).restart()
}

fun updateVolarLspAsync(project: Project) {
ApplicationManager.getApplication().invokeLater(Runnable {
updateVolarLsp(project, isVolarSettingEnabled(project))
}, project.disposed)
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.vuejs.lang.typescript.service.volar

import com.intellij.lang.javascript.ecmascript6.TypeScriptAnnotatorCheckerProvider
import com.intellij.lang.javascript.validation.JSEmptyTypeChecker
import com.intellij.lang.javascript.validation.JSProblemReporter
import com.intellij.lang.javascript.validation.JSTypeChecker
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiUtilCore
import org.jetbrains.vuejs.lang.typescript.service.isVolarEnabled
import org.jetbrains.vuejs.lang.typescript.service.isVolarFileTypeAcceptable

class VolarTypeScriptAnnotationProvider : TypeScriptAnnotatorCheckerProvider() {
override fun getTypeChecker(reporter: JSProblemReporter<*>): JSTypeChecker {
return JSEmptyTypeChecker.getInstance()
}

override fun skipErrors(context: PsiElement?) = true

override fun isAvailable(context: PsiElement): Boolean {
val virtualFile = PsiUtilCore.getVirtualFile(context) ?: return false
return isVolarEnabled(context.project, virtualFile)
}
}
41 changes: 41 additions & 0 deletions vuejs/src/org/jetbrains/vuejs/options/VueConfigurable.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.vuejs.options

import com.intellij.openapi.application.ApplicationBundle
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.options.UiDslUnnamedConfigurable
import com.intellij.openapi.project.Project
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.bind
import org.jetbrains.vuejs.VueBundle

class VueConfigurable(project: Project) : UiDslUnnamedConfigurable.Simple(), Configurable {
private val settings = getVueSettings(project)

override fun Panel.createContent() {
group(VueBundle.message("vue.configurable.service.group")) {
buttonsGroup {
row {
radioButton(VueBundle.message("vue.configurable.service.disabled"), VueServiceSettings.DISABLED)
contextHelp(VueBundle.message("vue.configurable.service.disabled.help"))
}
row {
radioButton(VueBundle.message("vue.configurable.service.auto"), VueServiceSettings.AUTO)
contextHelp(VueBundle.message("vue.configurable.service.auto.help"))
}
row {
radioButton(VueBundle.message("vue.configurable.service.volar"), VueServiceSettings.VOLAR)
contextHelp(VueBundle.message("vue.configurable.service.volar.help"))
}
row {
radioButton(VueBundle.message("vue.configurable.service.ts"), VueServiceSettings.TS_SERVICE)
contextHelp(VueBundle.message("vue.configurable.service.ts.help"))
}
}.bind(settings::serviceType)
}
}



override fun getDisplayName() = VueBundle.message("vue.configurable.title")
}

0 comments on commit 481ca95

Please sign in to comment.