-
Notifications
You must be signed in to change notification settings - Fork 395
Twirl support in metals #7751
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
Draft
ajafri2001
wants to merge
23
commits into
scalameta:main
Choose a base branch
from
ajafri2001:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Twirl support in metals #7751
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
2bbfab6
solid progress
ajafri2001 3923c42
Improved formatting
ajafri2001 59ed984
Added TwirlSuite
ajafri2001 ac617ee
Wip commit
ajafri2001 c6fe993
completions don't work
ajafri2001 b787d54
removed vars
ajafri2001 ba91d3c
wip add tests and docs
ajafri2001 6af579c
added play imports
ajafri2001 5850a63
format
ajafri2001 45c3364
format
ajafri2001 28de74f
WIP - Added play's DI
ajafri2001 8984c6b
wip - fixed twirl imports
ajafri2001 306300d
remove publishDiagnostics for twirl files
ajafri2001 e61bed8
added tests
ajafri2001 a1b9ac8
Merge branch 'main' into metals-play
ajafri2001 a2a181f
Merge pull request #2 from ajafri2001/metals-play
ajafri2001 9fdeab3
remove duplicate test from completionlspsuite.scala
ajafri2001 3c1bce9
fix tests
ajafri2001 921ebb1
add import and rename tests
ajafri2001 7329e96
fix twirl-hover-method test
ajafri2001 0ef0e15
Accomodate review changes
ajafri2001 113c98d
Merge branch 'scalameta:main' into main
ajafri2001 cc3f048
remove helper method
ajafri2001 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
171 changes: 171 additions & 0 deletions
171
metals/src/main/scala/scala/meta/internal/metals/TwirlAdjustments.scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| package scala.meta.internal.metals | ||
|
|
||
| import java.io.File | ||
|
|
||
| import scala.io.Codec | ||
|
|
||
| import scala.meta.inputs.Input.VirtualFile | ||
|
|
||
| import org.eclipse.lsp4j.Position | ||
| import play.twirl.compiler.GeneratedSourceVirtual | ||
| import play.twirl.compiler.TwirlCompiler | ||
|
|
||
| /** | ||
| * A utility object for adjusting and mapping positions between Twirl templates and their compiled Scala output. | ||
| * | ||
| * This is particularly useful for hover, completions and goto definition features between user-authored `.scala.html` | ||
| * templates and their generated `.template.scala` counterparts. | ||
| */ | ||
|
|
||
| object TwirlAdjustments { | ||
|
|
||
| def isPlayProject(implicit file: VirtualFile): Boolean = | ||
| file.path.contains("views/") | ||
|
|
||
| /** | ||
| * Probably not the best solution, as ideally one should also be able to take configuration from | ||
| * the client's build.sbt files as TwirlKeys.templateImports. But this works nonetheless. | ||
| */ | ||
| private def playImports( | ||
| originalImports: Seq[String] | ||
| )(implicit file: VirtualFile): Seq[String] = { | ||
| if (isPlayProject) | ||
| originalImports ++ Seq( | ||
| "models._", "controllers._", "play.api.i18n._", "views.html._", | ||
| "play.api.templates.PlayMagic._", "play.api.mvc._", "play.api.data._", | ||
| ) | ||
| else originalImports | ||
| } | ||
|
|
||
| private def playDI(implicit file: VirtualFile): Seq[String] = { | ||
| if (isPlayProject) Seq("@javax.inject.Inject()") | ||
| else Nil | ||
| } | ||
|
|
||
| /** | ||
| * Compiles an in-memory Twirl template into a compiled representation using the Twirl compiler. | ||
| * | ||
| * This method uses a virtual file and a resolved Scala version to invoke `TwirlCompiler.compileVirtual`. | ||
| * | ||
| * @param the virtual file representing the template content | ||
| * @param the full Scala version string (used to resolve compatibility with Twirl) | ||
| * @return the result of compiling the Twirl template | ||
| */ | ||
| def getCompiledString(implicit | ||
| file: VirtualFile, | ||
| scalaVersion: String, | ||
| ): GeneratedSourceVirtual = | ||
| TwirlCompiler | ||
| .compileVirtual( | ||
| content = file.value, | ||
| source = new File("foo/bar/example.scala.html"), | ||
| sourceDirectory = new File("foo/bar"), | ||
| resultType = "play.twirl.api.Html", | ||
| formatterType = "play.twirl.api.HtmlFormat.Appendable", | ||
| additionalImports = | ||
| playImports(TwirlCompiler.defaultImports(scalaVersion)), | ||
| constructorAnnotations = playDI, | ||
| codec = Codec( | ||
| scala.util.Properties.sourceEncoding | ||
| ), | ||
| scalaVersion = Some(scalaVersion), | ||
| inclusiveDot = true, | ||
| ) | ||
|
|
||
| /** | ||
| * Converts a character offset (index) in a string to an LSP `Position` (0 based - line number and character offset). | ||
| * | ||
| * @param The full text content. Can be either the Twirl Source or the Compiled Twirl File | ||
| * @param The character offset within the text (0-based). | ||
| * @return A `Position` object representing the line and column corresponding to the given index. | ||
| */ | ||
| private def getPositionFromIndex(text: String, index: Int): Position = { | ||
| val lines = text.substring(0, index).split('\n') | ||
| new Position(lines.length - 1, lines.last.length) | ||
| } | ||
|
|
||
| /** | ||
| * Converts an LSP `Position` (0 based - line number and character offset) into a character index. | ||
| * | ||
| * @param The full text content. Can be either the Twirl Source or the Compiled Twirl File | ||
| * @param The LSP `Position` to convert (line and character). | ||
| * @return The absolute character index in the string corresponding to the position. | ||
| */ | ||
| private def getIndexFromPosition(text: String, pos: Position): Int = { | ||
| val lines = text.split('\n') | ||
| lines | ||
| .take(pos.getLine) | ||
| .map(_.length + 1) | ||
| .sum + pos.getCharacter | ||
| } | ||
|
|
||
| val pattern = """(\d+)->(\d+)""".r | ||
|
|
||
| /** | ||
| * Extracts a positional mapping matrix from the compiled Twirl template. | ||
| * | ||
| * This method parses those mappings and builds a matrix of (original, generated) index pairs. | ||
| * The mapping is later used for position translation between source and compiled files. | ||
| * | ||
| * @param The compiled Twirl template content as a string | ||
| * @return An array of tuples representing (originalIndex, generatedIndex) pairs | ||
| */ | ||
| private def getMatrix(compiledTwirl: String): Array[(Int, Int)] = { | ||
| val number_matching = | ||
| pattern.findAllIn(compiledTwirl).toArray | ||
| val chars = number_matching.take(number_matching.length / 2) | ||
| chars.map { char => | ||
| val parts = char.split("->") | ||
| val a = parts(0).toInt | ||
| val b = parts(1).toInt | ||
| (b, a) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Maps positions between original Twirl template and compiled Scala output. | ||
| * | ||
| * Returns a tuple of: | ||
| * - The compiled virtual file, | ||
| * - A function to map original Twirl positions -> compiled Scala positions, | ||
| * - An AdjustedLspData instance for reverse mapping compiled Scala -> Twirl positions. | ||
| */ | ||
| def apply( | ||
| twirlFile: VirtualFile, | ||
| rawScalaVersion: String, | ||
| ): (VirtualFile, Position => Position, AdjustLspData) = { | ||
|
|
||
| val originalTwirl = twirlFile.value | ||
| val compiledSource = getCompiledString(twirlFile, rawScalaVersion) | ||
| val compiledTwirl = compiledSource.content | ||
| val newVirtualFile = twirlFile.copy(value = compiledTwirl) | ||
| val matrix: Array[(Int, Int)] = getMatrix(compiledTwirl) | ||
|
|
||
| /** | ||
| * Maps a Position in the original Twirl template to the corresponding | ||
| * Position in the compiled Scala output | ||
| */ | ||
| def mapPosition(originalPos: Position): Position = { | ||
| val originalIndex = getIndexFromPosition(originalTwirl, originalPos) | ||
| val idx = matrix.indexWhere(_._1 >= originalIndex) | ||
| val (origBase, genBase) = matrix(idx - 1) | ||
| val mappedIndex = genBase + (originalIndex - origBase) | ||
| getPositionFromIndex(compiledTwirl, mappedIndex) | ||
| } | ||
|
|
||
| /** | ||
| * Maps a Position in the compiled Scala output back to the original | ||
| */ | ||
| def reverseMapPosition(compiledPos: Position): Position = { | ||
| val compiledIndex = getIndexFromPosition(compiledTwirl, compiledPos) | ||
| val pos_tuple = compiledSource.mapPosition(compiledIndex) | ||
| getPositionFromIndex(originalTwirl, pos_tuple) | ||
| } | ||
|
|
||
| ( | ||
| newVirtualFile, | ||
| mapPosition, | ||
| AdjustedLspData.create(reverseMapPosition), | ||
| ) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AS an improvement, we could check in sbt project for play specific libraries. But this seems good enough for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That does make sense.