Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Language injections: support IDEA language injections #482

Merged
merged 8 commits into from
May 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
<Compile Include="src\Injected\FSharpLiteralInjectionTarget.fs" />
<Compile Include="src\Injected\FSharpRegexProviders.fs" />
<Compile Include="src\Injected\FSharpRegexNodeProvider.fs" />
<Compile Include="src\Injected\FSharpInjectionTargetsFinderFactory.fs" />
<Compile Include="src\FSharpTypingAssist.fs" />
<Compile Include="src\ZoneMarker.fs" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
namespace JetBrains.ReSharper.Plugins.FSharp.Psi.Features.Injected

open System
open JetBrains.Application
open JetBrains.ReSharper.Plugins.FSharp.Psi.Injections
open JetBrains.ReSharper.Plugins.FSharp.Util
open JetBrains.ReSharper.Plugins.FSharp.Psi
open JetBrains.ReSharper.Psi
open JetBrains.ReSharper.Psi.CodeAnnotations
open JetBrains.ReSharper.Psi.Tree
open JetBrains.ReSharper.Plugins.FSharp.Psi.Impl
open JetBrains.ReSharper.Plugins.FSharp.Psi.Tree
open JetBrains.ReSharper.Psi.impl.Shared.InjectedPsi
open JetBrains.ReSharper.Plugins.FSharp.Psi.Features.Daemon.Highlightings.FSharpErrorUtil
open JetBrains.ReSharper.Plugins.FSharp.Psi.Features.Injected.FSharpInjectionAnnotationUtil

type FSharpInjectionTargetsFinder() =
let possibleInjectorFunctionNames = [|"html"; "css"; "sql"; "javascript"; "json"; "jsx"|]
let normalizeLanguage = function
| "js" | "jsx" -> "javascript"
| language -> language

let checkForAttributes (expr: IFSharpExpression) =
match getAttributesOwner expr with
| ValueNone -> ValueNone
| ValueSome attributesOwner ->

let info = getAnnotationInfo<StringSyntaxAnnotationProvider, string>(attributesOwner)
if isNotNull info then ValueSome(info, "", "") else
let info = getAnnotationInfo<LanguageInjectionAnnotationProvider, InjectionAnnotationInfo>(attributesOwner)
if isNotNull info then ValueSome(info.Language, info.Prefix, info.Suffix) else ValueNone

static member val Instance = FSharpInjectionTargetsFinder()

interface ILanguageInjectionTargetsFinder with
member this.Find(searchRoot, consumer) =
let mutable descendants = searchRoot.CompositeDescendants()
while descendants.MoveNext() do
Interruption.Current.CheckAndThrow()

match descendants.Current with
| :? IInjectionHostNode as expr when expr.IsValidHost ->
match checkForAttributes expr with
| ValueSome(language, prefix, suffix) when not (equalsIgnoreCase language "Regex") ->
consumer.Consume(expr, normalizeLanguage language, prefix, suffix)
| _ ->

let prefixApp = PrefixAppExprNavigator.GetByArgumentExpression(expr.IgnoreParentParens())
if isNotNull prefixApp then
// support injection functions
// https://github.com/alfonsogarciacaro/vscode-template-fsharp-highlight
match prefixApp.FunctionExpression.IgnoreInnerParens() with
| :? IReferenceExpr as ref when isSimpleQualifiedName ref ->
let language = normalizeLanguage ref.ShortName
if Array.contains language possibleInjectorFunctionNames then
consumer.Consume(expr, language, "", "")
| _ -> ()
else match tryGetTypeProviderName (expr.As<IConstExpr>()) with
| ValueSome "SqlCommandProvider" ->
consumer.Consume(expr, "sql", "", "")
| ValueSome "JsonProvider" when expr.GetText().Contains("{") ->
consumer.Consume(expr, "json", "", "")
| ValueSome "XmlProvider" when expr.GetText().Contains("<") ->
consumer.Consume(expr, "xml", "", "")
| _ -> ()

| :? IChameleonNode as c when not c.IsOpened -> descendants.SkipThisNode()
| _ -> ()


[<Language(typeof<FSharpLanguage>)>]
type FSharInjectionTargetsFinderFactory() =
interface ILanguageInjectionTargetsFinderFactory with
member this.CreateAnnotationTargetsFinder() = FSharpInjectionTargetsFinder.Instance
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ open JetBrains.ReSharper.Psi
open JetBrains.ReSharper.Psi.CodeAnnotations
open JetBrains.ReSharper.Psi.Tree
open JetBrains.ReSharper.Plugins.FSharp.Psi.Features.Util.FSharpMethodInvocationUtil
open JetBrains.ReSharper.Plugins.FSharp.Psi.Impl

let getAnnotationInfo<'AnnotationProvider, 'TAnnotationInfo
when 'AnnotationProvider :> CodeAnnotationInfoProvider<IAttributesOwner, 'TAnnotationInfo>>
Expand Down Expand Up @@ -37,3 +38,12 @@ let getAttributesOwner (expr: IFSharpExpression) =

if isNull declaration then ValueNone else
declaration.DeclaredElement.As<IAttributesOwner>() |> ValueOption.ofObj

let tryGetTypeProviderName (expr: IConstExpr) =
let providedTypeName =
ExprStaticConstantTypeUsageNavigator.GetByExpression(expr)
|> PrefixAppTypeArgumentListNavigator.GetByTypeUsage
|> TypeReferenceNameNavigator.GetByTypeArgumentList

if isNotNull providedTypeName then ValueSome (providedTypeName.Identifier.GetSourceName())
else ValueNone
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,6 @@ type FSharpRegexNodeProvider() =
let isRegexActivePat = isNotNull parametersOwnerPat && parametersOwnerPat.Identifier.GetSourceName() = "Regex"
if isRegexActivePat then ValueSome RegexOptions.None else ValueNone

let checkForRegexTypeProvider (expr: IConstExpr) =
let providedTypeName =
ExprStaticConstantTypeUsageNavigator.GetByExpression(expr)
|> PrefixAppTypeArgumentListNavigator.GetByTypeUsage
|> TypeReferenceNameNavigator.GetByTypeArgumentList

let isRegexProvider = isNotNull providedTypeName && providedTypeName.Identifier.GetSourceName() = "Regex"
if isRegexProvider then ValueSome RegexOptions.None else ValueNone

interface IInjectionNodeProvider with
override _.Check(node, _, data) =
data <- null
Expand All @@ -96,7 +87,10 @@ type FSharpRegexNodeProvider() =
| :? ILiteralExpr as expr ->
let checkAttributesResult = checkForAttributes expr
if checkAttributesResult.IsSome then checkAttributesResult else
checkForRegexTypeProvider expr

match tryGetTypeProviderName expr with
| ValueSome "Regex" -> ValueSome RegexOptions.None
| _ -> ValueNone

| :? ILiteralPat as pat -> checkForRegexActivePattern pat
| _ -> ValueNone
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using JetBrains.DocumentModel;
using JetBrains.ReSharper.Plugins.FSharp.Psi.Injections;
using JetBrains.ReSharper.Plugins.FSharp.Psi.Parsing;
using JetBrains.ReSharper.Psi.Tree;

Expand Down Expand Up @@ -27,5 +28,7 @@ public DocumentRange GetDollarSignRange()
? startOffset.ExtendRight(+1)
: startOffset.Shift(+1).ExtendRight(+1);
}

bool IInjectionHostNode.IsValidHost => true;
}
}
9 changes: 8 additions & 1 deletion ReSharper.FSharp/src/FSharp.Psi/src/Impl/Tree/LiteralExpr.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using JetBrains.ReSharper.Plugins.FSharp.Psi.Injections;
using JetBrains.ReSharper.Plugins.FSharp.Psi.Parsing;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.ExtensionsAPI.Tree;
Expand Down Expand Up @@ -110,7 +111,7 @@ public override ConstantValue ConstantValue

try
{
var result = Convert.ToInt32(literalText, (int) literalBase);
var result = Convert.ToInt32(literalText, (int)literalBase);
return ConstantValue.Create(result, GetPsiModule().GetPredefinedType().Int);
}
catch (Exception)
Expand Down Expand Up @@ -147,5 +148,11 @@ private enum IntBase
Decimal = 10,
Hexadecimal = 16
}

bool IInjectionHostNode.IsValidHost =>
Literal?.GetTokenType() is { } tokenType &&
(tokenType == FSharpTokenType.STRING ||
tokenType == FSharpTokenType.VERBATIM_STRING ||
tokenType == FSharpTokenType.TRIPLE_QUOTED_STRING);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using JetBrains.ReSharper.Plugins.FSharp.Psi.Tree;

namespace JetBrains.ReSharper.Plugins.FSharp.Psi.Injections
{
public interface IInjectionHostNode: IFSharpExpression
{
bool IsValidHost { get; }
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using JetBrains.DocumentModel;
using JetBrains.ReSharper.Plugins.FSharp.Psi.Injections;

namespace JetBrains.ReSharper.Plugins.FSharp.Psi.Tree
{
public partial interface IInterpolatedStringExpr
public partial interface IInterpolatedStringExpr : IInjectionHostNode
{
public bool IsTrivial();
public DocumentRange GetDollarSignRange();
Expand Down
8 changes: 8 additions & 0 deletions ReSharper.FSharp/src/FSharp.Psi/src/Tree/ILiteralExpr.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using JetBrains.ReSharper.Plugins.FSharp.Psi.Injections;

namespace JetBrains.ReSharper.Plugins.FSharp.Psi.Tree
{
public partial interface ILiteralExpr : IInjectionHostNode
{
}
}
10 changes: 9 additions & 1 deletion rider-fsharp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,15 @@ intellij {

// rider-plugins-appender: workaround for https://youtrack.jetbrains.com/issue/IDEA-179607
// org.intellij.intelliLang needed for tests with language injection marks
plugins.set(listOf("rider-plugins-appender", "org.intellij.intelliLang"))
plugins.set(
listOf(
"rider-plugins-appender",
"org.intellij.intelliLang",
"DatabaseTools",
"css-impl",
"javascript-impl"
)
)
}

val repoRoot = projectDir.parentFile!!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@ import com.intellij.lang.PsiBuilder
import com.intellij.lang.PsiParser
import com.intellij.psi.tree.IElementType
import com.jetbrains.rider.ideaInterop.fileTypes.fsharp.lexer.FSharpTokenType
import com.jetbrains.rider.ideaInterop.fileTypes.fsharp.psi.*
import com.jetbrains.rider.ideaInterop.fileTypes.fsharp.psi.impl.FSharpElementTypes
import com.jetbrains.rider.ideaInterop.fileTypes.fsharp.psi.parse
import com.jetbrains.rider.ideaInterop.fileTypes.fsharp.psi.parseEvenEmpty
import com.jetbrains.rider.ideaInterop.fileTypes.fsharp.psi.scanOrRollback
import com.jetbrains.rider.ideaInterop.fileTypes.fsharp.psi.whileMakingProgress

class FSharpDummyParser : PsiParser {
override fun parse(root: IElementType, builder: PsiBuilder): ASTNode {
Expand Down Expand Up @@ -72,12 +69,17 @@ class FSharpDummyParser : PsiParser {
// secondStringOperandIndent - afterPlusTokenIndent > 1
//) return false

return parseStringExpression()
return parseConcatenationOperand()
}

private fun PsiBuilder.parseStringExpression() =
parseInterpolatedStringExpression() || parseAnyStringExpression()

// We want to support concatenation with simple identifiers, similar to C#
// for example, "select * from table where columnId = " + columnId
private fun PsiBuilder.parseConcatenationOperand() =
tryEatAnyToken(FSharpTokenType.IDENT) || parseStringExpression()

private fun PsiBuilder.parseAnyStringExpression() =
if (tokenType !in FSharpTokenType.ALL_STRINGS) false
else parse {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.jetbrains.rider.ideaInterop.fileTypes.fsharp.injections

import com.intellij.openapi.util.TextRange
import com.intellij.psi.ElementManipulators
import com.intellij.psi.PsiElement
import com.jetbrains.rider.ideaInterop.fileTypes.common.psi.ClrLanguageInterpolatedStringLiteralExpression
import com.jetbrains.rider.ideaInterop.fileTypes.common.psi.ClrLanguageInterpolatedStringLiteralExpressionPart
import com.jetbrains.rider.ideaInterop.fileTypes.fsharp.lexer.FSharpTokenType.*
import com.jetbrains.rider.ideaInterop.fileTypes.fsharp.psi.FSharpInterpolatedStringLiteralExpressionPart
import com.jetbrains.rider.ideaInterop.fileTypes.fsharp.psi.FSharpStringLiteralExpression
import com.jetbrains.rider.plugins.appender.database.common.ClrLanguageConcatenationAwareInjector
import org.intellij.plugins.intelliLang.inject.config.BaseInjection

class FSharpConcatenationAwareInjector :
ClrLanguageConcatenationAwareInjector(FSharpInjectionSupport.FSHARP_SUPPORT_ID) {
override fun getInjectionProcessor(injection: BaseInjection) = FSharpInjectionProcessor(injection)
override fun isInjectionHost(concatenationOperand: PsiElement) = concatenationOperand is FSharpStringLiteralExpression

protected class FSharpInjectionProcessor(injection: BaseInjection) :
InjectionProcessor(
injection,
mapOf(SLASH_NEWLINE to "\\\\\\n", PLACEHOLDER_IDENTIFIER to "\\{\\d+}"),
mapOf(SLASH_NEWLINE to "\\\\\\n")
) {

override fun getFragmentStartAfterTemplate(matchResult: MatchResult) =
if (matchResult.groups[SLASH_NEWLINE] != null) matchResult.range.last
else super.getFragmentStartAfterTemplate(matchResult)

override fun getPlaceholderForTemplate(matchResult: MatchResult) =
if (matchResult.groups[SLASH_NEWLINE] != null) " "
else PLACEHOLDER_IDENTIFIER

override fun disableInspections(matchResult: MatchResult) =
matchResult.groups[SLASH_NEWLINE] == null

override fun getInterpolatedStringPartTextRange(
literal: ClrLanguageInterpolatedStringLiteralExpression,
part: ClrLanguageInterpolatedStringLiteralExpressionPart
): TextRange {
val wholeLiteralRange = ElementManipulators.getValueTextRange(literal)
return when (part) {
is FSharpInterpolatedStringLiteralExpressionPart -> {
val partLength = part.textLength
val partText = part.text

if (part.tokenType in INTERPOLATED_STRINGS_WITHOUT_INSERTIONS)
return wholeLiteralRange

val startOffsetInPart =
if (part.tokenType in INTERPOLATED_STRING_STARTS) {
// can't reliably inspect injected PSI with interpolations
disableInspections = true
wholeLiteralRange.startOffset
} else part.startOffsetInParent + 1

val endOffsetInPart =
if (part.tokenType in INTERPOLATED_STRING_ENDS) wholeLiteralRange.endOffset
else {
val containsFormatSpecifier =
partLength > 3 && partText[partLength - 3] == '%' && partText[partLength - 2].isLetter()
part.startOffsetInParent + partLength - 1 - (if (containsFormatSpecifier) 2 else 0)
}

TextRange(startOffsetInPart, endOffsetInPart)
}

else -> error("Unexpected interpolated part type: $part")
}
}

companion object {
const val SLASH_NEWLINE = "slashNewLine"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.jetbrains.rider.ideaInterop.fileTypes.fsharp.injections

import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.jetbrains.rider.ideaInterop.fileTypes.fsharp.lexer.FSharpTokenType
import com.jetbrains.rider.ideaInterop.fileTypes.fsharp.psi.FSharpExpression
import com.jetbrains.rider.ideaInterop.fileTypes.fsharp.psi.FSharpStringLiteralExpression
import com.jetbrains.rider.plugins.appender.database.common.ClrLanguageConcatenationToInjectorAdapter

class FSharpConcatenationToInjectorAdapter(project: Project) :
ClrLanguageConcatenationToInjectorAdapter(project, FSharpTokenType.PLUS) {
override fun elementsToInjectIn() = arrayListOf(FSharpStringLiteralExpression::class.java)
override fun isConcatenationExpression(element: PsiElement) = element is FSharpExpression
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.jetbrains.rider.ideaInterop.fileTypes.fsharp.injections

import com.intellij.psi.PsiLanguageInjectionHost
import com.jetbrains.rider.ideaInterop.fileTypes.common.psi.patterns.ClrLanguagePatterns
import com.jetbrains.rider.ideaInterop.fileTypes.fsharp.lexer.FSharpTokenType
import com.jetbrains.rider.ideaInterop.fileTypes.fsharp.psi.FSharpStringLiteralExpression
import com.jetbrains.rider.plugins.appender.database.common.ClrLanguageInjectionSupport

object FSharpPatterns : ClrLanguagePatterns(FSharpTokenType.PLUS){
@JvmStatic
@Suppress("unused")
fun fsharpSqlInStringPattern() = super.sqlInStringPattern()
}

class FSharpInjectionSupport : ClrLanguageInjectionSupport() {
override fun getPatternClasses() = arrayOf(FSharpPatterns.javaClass)
override fun getId() = FSHARP_SUPPORT_ID
override fun isApplicableTo(host: PsiLanguageInjectionHost?) = host is FSharpStringLiteralExpression

companion object {
const val FSHARP_SUPPORT_ID = "F#"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.jetbrains.rider.ideaInterop.fileTypes.fsharp.injections

import com.jetbrains.rider.ideaInterop.fileTypes.fsharp.FSharpLanguage
import com.jetbrains.rider.plugins.appender.database.common.ClrLanguageSqlDapperParameterResolveExtension

class FSharpSqlDapperParameterResolveExtension : ClrLanguageSqlDapperParameterResolveExtension(FSharpLanguage)
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,12 @@ public interface FSharpTokenType {

TokenSet ALL_STRINGS = TokenSet.orSet(STRINGS, INTERPOLATED_STRINGS);

TokenSet INTERPOLATED_STRINGS_WITHOUT_INSERTIONS = TokenSet.create(
REGULAR_INTERPOLATED_STRING,
VERBATIM_INTERPOLATED_STRING,
TRIPLE_QUOTE_INTERPOLATED_STRING
);

TokenSet INTERPOLATED_STRING_STARTS = TokenSet.create(
REGULAR_INTERPOLATED_STRING_START,
VERBATIM_INTERPOLATED_STRING_START,
Expand Down
Loading