-
-
Notifications
You must be signed in to change notification settings - Fork 31
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
Implement basic one-off language inections #82
base: master
Are you sure you want to change the base?
Conversation
Not sure why it is not passing in CI, |
Doesn't look related to your changes. I just triggered a re-build and it succeeded. |
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.
Hi, big thanks for your contribution. Unfortunatly, I am abit distracted this week. I may not always respond as fast I would like. Just that you know. Anyway, here is my initial quick review.
Indentation when decoding Nix indented string is not implemented. I think this might simply not be possible, because with my attempts editor just crashed.
I think IDEA supports that for text blocks in Java. However, that is a rather new think. If I remember correctly, I think IDEA doesn't yet support that for Kotlin. I could imagine that they use a different (maybe internal) API in case of Java. I don't know. I didn't yet really look at the API you are using.
Another potential issue is that I implemented much of the boilerplate in Kotlin, because I am more at ease writing in it. It's not super in line with the rest of the code, so I am happy to re-write in Java now that the actual development is done, if you prefer.
I don't think that is necessary.
I am happy to keep working on this PR to implement the things above, or do separate PRs so they are smaller and easier to review. See some pictures below.
Not sure. I am happy to integrate this PR as long as it doesn't break anything.
|
||
class NixStringLiteralEscaper(host: AbstractNixString) : LiteralTextEscaper<PsiLanguageInjectionHost>(host) { | ||
|
||
override fun isOneLine(): Boolean = false |
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.
question (non-blocking): What is this used for? Some strings may only be one line, right? Maybe we should return true
for NixStdString
?
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.
I took the example from HCL which also always returns false. I will try to implement proper smart logic, see if that makes any difference, but I suspected it might be best to copy my reference for now 🤷
if (myHost is NixIndString) { | ||
outChars.append(NixIndStringUtil.escape(subText)) | ||
} else { | ||
NixStringUtil.escape(outChars, subText) |
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.
issue: I suspect that you may want to unescape the string here, instead of escaping it.
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.
Ah I misnamed NixIndStringUtil.escape()
, it should be unescape. I'll fix that, good catch.
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.
In this case, I would suggest using NixStringUtil.parse
, as it already supports both types of strings. You would have to iterate over NixString.getStringParts()
. Each part implements either NixStringText
or NixAntiquotation
. The first one can be unescaped by NixStringUtil.parse
. The second one represents a string interpolation, not sure what to do with it.
@@ -93,7 +94,7 @@ public static void escape(@NotNull StringBuilder builder, @NotNull CharSequence | |||
* @param textNode A part of a string. | |||
* @return The resulting string after resolving all escape sequences. | |||
*/ | |||
public static @NotNull String parse(@NotNull NixStringText textNode) { | |||
public static @NotNull String parse(@NotNull NixStringPart textNode) { |
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.
issue: This seems wrong, if I am not missing something. NixStringPart
may also be a NixAntiquotation
, which is how interpolations were called by Nix in the past. This means this method my now run into a runtime exception: “Unexpected token in string”.
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.
You are correct, good catch. I needed to change this for
NixStringManipulator.handleContentChange()
but that's not implementet yet, so I will revert this.
} | ||
|
||
override fun getRangeInElement(element: NixString): TextRange = when { | ||
element.textLength == 0 -> TextRange.EMPTY_RANGE |
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.
note: This first case seems unreachable to me.
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.
To me too (and it should be unreachable in the HCL plugin too, but they also do it there).
My guess is they just wanted to cover all the codepaths?
Or maybe it's possible to have non-compiling code that returns zero-length tokens.
element is NixIndString -> TextRange(2, element.textLength - 2) | ||
// element is not IndString, so it must be StdString | ||
element.textLength == 1 -> TextRange(0, 1) | ||
else -> TextRange(1, element.textLength - 1) |
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.
question (non-blocking): Is it important that the returned range only contains the content of the string? Just wondering because this looks as you may return the quotes as part of the range in some cases. (Specifically when the string is not closed.)
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.
correct, I also don't know what the consequences of this are. My reference has the same.
I can always cut out quotes if you want, I have no opinion here
change wording in comment undo change signature of NixStringUtil.parse remove remnant in NixIndStringUtilTest make NixIndStringUtil.escape JvmStatic
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.
Thanks for your thorough review @JojOatXGME !
I may not always respond as fast I would like
No hurry at all! I will tag you for review when I think it is ready again but take as long as you need to get back to me. Your reviews are thorough so it must be time consuming. I will be offline next week myself.
I didn't yet really look at the API you are using.
I am using the Java API so I don't think the problem is there. I suspect dealing with the offsets needs special treatment when passing the decoded text back to the editor (otherwise you might be giving it different ranges it expects?). I am not fully clear on the inner workings of those yet. I will do a bit more exploration, but I suspect I need to implement decode()
while manually keeping track of offsets like here
} | ||
|
||
override fun getRangeInElement(element: NixString): TextRange = when { | ||
element.textLength == 0 -> TextRange.EMPTY_RANGE |
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.
To me too (and it should be unreachable in the HCL plugin too, but they also do it there).
My guess is they just wanted to cover all the codepaths?
Or maybe it's possible to have non-compiling code that returns zero-length tokens.
element is NixIndString -> TextRange(2, element.textLength - 2) | ||
// element is not IndString, so it must be StdString | ||
element.textLength == 1 -> TextRange(0, 1) | ||
else -> TextRange(1, element.textLength - 1) |
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.
correct, I also don't know what the consequences of this are. My reference has the same.
I can always cut out quotes if you want, I have no opinion here
@@ -93,7 +94,7 @@ public static void escape(@NotNull StringBuilder builder, @NotNull CharSequence | |||
* @param textNode A part of a string. | |||
* @return The resulting string after resolving all escape sequences. | |||
*/ | |||
public static @NotNull String parse(@NotNull NixStringText textNode) { | |||
public static @NotNull String parse(@NotNull NixStringPart textNode) { |
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.
You are correct, good catch. I needed to change this for
NixStringManipulator.handleContentChange()
but that's not implementet yet, so I will revert this.
Hi, looked a bit through the source code of IntelliJ Community to understand how they are handling string interpolations for Java. I noticed the following;
So, the implementation for Injections in Java seem rather complex, but I didn't yet understand for what all this complexity is for. I hope there is a simpler solution to properly support strings containing interpolations. Anyway, I wanted to share with you what I noticed, although I hoped to provide more specific guidance. I haven't really looked at your reference (Terraform HCL plugin), if they also have injections, maybe there implementation is easier to understand. |
Hello! Sorry for the delay. Thanks for taking a look at the Java source.
I think they do this to be able to perforn injections in concatenated strings, like in I also think it adds complexity, because before when decoding the string (ie, telling IDEA "this is what the string is supposed to be in YAML") you need to concatenate the fragments again. Let me know if this is also your understanding too.
From reading the IDEA docs, I believe this is necessary to add 'annotated' permanent injections, like Kotlin has too: @Language("YAML")
val someConfig = """
hello:
- I am
- a language
- injection
""" I would like this for Nix, but it increeases the complexity a lot like you said, so if I get to it it will be in a separate PR.
For this and the latter part of (4), I think that's for injections inside string concatenation too. As for implementing indent-aware injections in the host, Java escapes its strings keeping track of stateful offsets here, which are populated during |
hey @JojOatXGME ! I think this is ready for another review
The trade-off is that the I have looked at how Java and Kotlin do injections in multiline strings too. They use a different approach (like you said, they work on the fragment directly). I have yet to understand how they handle string concatenation of each fragment, and how the interpolation plays into that. Their approach results in a slightly better UX where an interpolation in the host lang (Nix) is not highlighted as the guest lang (YAML, etc) when doing an injection. See the following secreenshots: Yaml in Kotlin (interpolation in yaml is highlighted): Yaml in Nix (interpolation looks like yaml text): Overall I think the former approach, used in this PR, is 'good enough' for now at least. It's also how Terraform does it, even though they also do have string interpolations. I am still happy to keep looking at the Java/Kotlin approach and come up with better injection in this PR or in a new one. Feel free to review whenever in your own time. Thank you! |
Thanks, I will look at it!
I am a bit concerned because this means we have two different modes while escaping the text. If a user adds |
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.
Unfortunately, I still found a few issues. The issue of the missing escaping in AbstractNixString.updateText
is probably tricky to resolve 😟. The other remaining topics seem rather trivial (or non-blocking). If you don't mind, I may try out some changes later today, as I got some ideas while looking at your PR.
// it uses different escaping mechanisms | ||
if (myHost !is NixIndString) return false | ||
|
||
val subText: String = rangeInsideHost.substring(myHost.text) |
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.
issue (non-blocking): I think this is technically wrong.
If it's impossible to properly decode some chars from the specified range (e. g. if the range starts or ends inside escaped sequence), decode the longest acceptable prefix of the range and return false
— Javadoc ofLiteralTextEscaper.decode
According to my understanding of the documentation, if the range starts between the first and last charachter of ''\n
, we should immediately return false
without modifying outChars
. However, I can imagine that this is only relevant in edge cases, which may not be that important for now.
val subText: String = rangeInsideHost.substring(myHost.text) | ||
val array = IntArray(subText.length + 1) | ||
val success = unescapeAndDecode(subText, outChars, array) | ||
outSourceOffsets = array |
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.
price: I like this solution of creating a lookup table.
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.
I wish I could take credit, I borrowed the idea from terraform
// if the first line was removed in the fragment, add it back to preserve a multiline string | ||
val withLeadingBlankLine = if (lines.first().isNotEmpty()) listOf("") + withIndent else withIndent | ||
|
||
originalNode?.replaceWithText(withLeadingBlankLine.joinToString(separator = System.lineSeparator())) |
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.
issue: This line throws an exception when using line feeds which don't match the default on the system. We probably have to use the line feeds configured in IntelliJ for the current file, instead of using System.lineSeparator()
. Or maybe it works to always use \n
. Not sure what replaceWithText
expects here.
See exception I got on Windows
java.lang.AssertionError: Wrong line separators: '\r\n first_...' at offset 0
at com.intellij.openapi.util.text.StringUtil.assertValidSeparators(StringUtil.java:2484)
at com.intellij.openapi.editor.impl.DocumentImpl.assertValidSeparators(DocumentImpl.java:706)
at com.intellij.openapi.editor.impl.DocumentImpl.replaceString(DocumentImpl.java:600)
at com.intellij.openapi.editor.impl.DocumentImpl.replaceString(DocumentImpl.java:591)
at com.intellij.psi.impl.PsiToDocumentSynchronizer.doCommitTransaction(PsiToDocumentSynchronizer.java:212)
at com.intellij.psi.impl.PsiToDocumentSynchronizer.lambda$commitTransaction$1(PsiToDocumentSynchronizer.java:188)
at com.intellij.psi.impl.PsiToDocumentSynchronizer.lambda$doSync$0(PsiToDocumentSynchronizer.java:106)
at com.intellij.psi.impl.PsiToDocumentSynchronizer.performAtomically(PsiToDocumentSynchronizer.java:124)
at com.intellij.psi.impl.PsiToDocumentSynchronizer.doSync(PsiToDocumentSynchronizer.java:106)
at com.intellij.psi.impl.PsiToDocumentSynchronizer.commitTransaction(PsiToDocumentSynchronizer.java:188)
at com.intellij.pom.core.impl.PomModelImpl.commitTransaction(PomModelImpl.java:195)
at com.intellij.pom.core.impl.PomModelImpl.lambda$runTransaction$1(PomModelImpl.java:151)
at com.intellij.psi.impl.DebugUtil.performPsiModification(DebugUtil.java:535)
at com.intellij.pom.core.impl.PomModelImpl.lambda$runTransaction$2(PomModelImpl.java:103)
at com.intellij.openapi.progress.impl.CoreProgressManager.lambda$executeNonCancelableSection$2(CoreProgressManager.java:228)
at com.intellij.openapi.progress.impl.CoreProgressManager.computeUnderProgress(CoreProgressManager.java:634)
at com.intellij.openapi.progress.impl.CoreProgressManager.lambda$computeInNonCancelableSection$3(CoreProgressManager.java:236)
at com.intellij.openapi.progress.Cancellation.computeInNonCancelableSection(Cancellation.java:53)
at com.intellij.openapi.progress.impl.CoreProgressManager.computeInNonCancelableSection(CoreProgressManager.java:236)
at com.intellij.openapi.progress.impl.CoreProgressManager.executeNonCancelableSection(CoreProgressManager.java:227)
at com.intellij.pom.core.impl.PomModelImpl.runTransaction(PomModelImpl.java:92)
at com.intellij.psi.impl.source.tree.ChangeUtil.prepareAndRunChangeAction(ChangeUtil.java:141)
at com.intellij.psi.impl.source.tree.CompositeElement.replaceChild(CompositeElement.java:632)
at com.intellij.psi.impl.source.tree.LeafElement.replaceWithText(LeafElement.java:138)
at org.nixos.idea.psi.impl.AbstractNixString.updateText(AbstractNixString.kt:43)
at org.nixos.idea.psi.impl.AbstractNixString.updateText(AbstractNixString.kt:12)
at org.nixos.idea.psi.NixStringManipulator.handleContentChange(NixStringManipulator.kt:24)
at org.nixos.idea.psi.NixStringManipulator.handleContentChange(NixStringManipulator.kt:10)
at com.intellij.psi.ElementManipulators.handleContentChange(ElementManipulators.java:65)
at com.intellij.psi.impl.source.tree.injected.changesHandler.CommonInjectedFileChangesHandler.updateHostElement(CommonInjectedFileChangesHandler.kt:170)
at com.intellij.psi.impl.source.tree.injected.changesHandler.CommonInjectedFileChangesHandler.updateHostOrFail(CommonInjectedFileChangesHandler.kt:149)
at com.intellij.psi.impl.source.tree.injected.changesHandler.CommonInjectedFileChangesHandler.commitToOriginal(CommonInjectedFileChangesHandler.kt:122)
at com.intellij.codeInsight.intention.impl.QuickEditHandler.lambda$commitToOriginal$10(QuickEditHandler.java:343)
at com.intellij.psi.impl.source.PostprocessReformattingAspectImpl.lambda$disablePostprocessFormattingInside$1(PostprocessReformattingAspectImpl.java:119)
at com.intellij.psi.impl.source.PostprocessReformattingAspectImpl.disablePostprocessFormattingInside(PostprocessReformattingAspectImpl.java:128)
at com.intellij.psi.impl.source.PostprocessReformattingAspectImpl.disablePostprocessFormattingInside(PostprocessReformattingAspectImpl.java:118)
at com.intellij.codeInsight.intention.impl.QuickEditHandler.commitToOriginal(QuickEditHandler.java:343)
at com.intellij.codeInsight.intention.impl.QuickEditHandler.documentChanged(QuickEditHandler.java:278)
at com.intellij.openapi.editor.impl.DocumentImpl.lambda$changedUpdate$1(DocumentImpl.java:913)
at com.intellij.openapi.progress.impl.CoreProgressManager.lambda$executeNonCancelableSection$2(CoreProgressManager.java:228)
at com.intellij.openapi.progress.impl.CoreProgressManager.registerIndicatorAndRun(CoreProgressManager.java:685)
at com.intellij.openapi.progress.impl.CoreProgressManager.computeUnderProgress(CoreProgressManager.java:641)
at com.intellij.openapi.progress.impl.CoreProgressManager.lambda$computeInNonCancelableSection$3(CoreProgressManager.java:236)
at com.intellij.openapi.progress.Cancellation.computeInNonCancelableSection(Cancellation.java:57)
at com.intellij.openapi.progress.impl.CoreProgressManager.computeInNonCancelableSection(CoreProgressManager.java:236)
at com.intellij.openapi.progress.impl.CoreProgressManager.executeNonCancelableSection(CoreProgressManager.java:227)
at com.intellij.openapi.editor.impl.DocumentImpl.changedUpdate(DocumentImpl.java:910)
at com.intellij.openapi.editor.impl.DocumentImpl.updateText(DocumentImpl.java:814)
at com.intellij.openapi.editor.impl.DocumentImpl.deleteString(DocumentImpl.java:570)
at com.intellij.openapi.editor.actions.BackspaceAction.lambda$doBackSpaceAtCaret$0(BackspaceAction.java:65)
at com.intellij.openapi.editor.ex.util.EditorUtil.runWithAnimationDisabled(EditorUtil.java:1015)
at com.intellij.openapi.editor.actions.BackspaceAction.doBackSpaceAtCaret(BackspaceAction.java:70)
at com.intellij.openapi.editor.actions.BackspaceAction$Handler.executeWriteAction(BackspaceAction.java:32)
at com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler$1.run(EditorWriteActionHandler.java:42)
at com.intellij.openapi.application.impl.ApplicationImpl.runWriteAction(ApplicationImpl.java:975)
at com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler.doExecute(EditorWriteActionHandler.java:56)
at com.intellij.openapi.editor.actionSystem.EditorActionHandler.execute(EditorActionHandler.java:202)
at com.intellij.database.run.SqlNotebookDeleteHandler.doExecute(SqlNotebookDeleteHandler.java:30)
at com.intellij.openapi.editor.actionSystem.EditorActionHandler.execute(EditorActionHandler.java:202)
at com.intellij.codeInsight.inline.completion.CancellationKeyInlineCompletionHandler.doExecute(InlineCompletionActions.kt:31)
at com.intellij.openapi.editor.actionSystem.EditorActionHandler.execute(EditorActionHandler.java:202)
at com.intellij.codeInsight.editorActions.BackspaceHandler.handleBackspace(BackspaceHandler.java:91)
at com.intellij.codeInsight.editorActions.BackspaceHandler.executeWriteAction(BackspaceHandler.java:47)
at com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler$1.run(EditorWriteActionHandler.java:42)
at com.intellij.openapi.application.impl.ApplicationImpl.runWriteAction(ApplicationImpl.java:975)
at com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler.doExecute(EditorWriteActionHandler.java:56)
at com.intellij.openapi.editor.actionSystem.EditorActionHandler.lambda$execute$2(EditorActionHandler.java:192)
at com.intellij.openapi.editor.actionSystem.EditorActionHandler.doIfEnabled(EditorActionHandler.java:89)
at com.intellij.openapi.editor.actionSystem.EditorActionHandler.lambda$execute$3(EditorActionHandler.java:191)
at com.intellij.openapi.editor.impl.CaretModelImpl.lambda$runForEachCaret$3(CaretModelImpl.java:302)
at com.intellij.openapi.editor.impl.CaretModelImpl.doWithCaretMerging(CaretModelImpl.java:411)
at com.intellij.openapi.editor.impl.CaretModelImpl.runForEachCaret(CaretModelImpl.java:311)
at com.intellij.openapi.editor.impl.CaretModelImpl.runForEachCaret(CaretModelImpl.java:288)
at com.intellij.openapi.editor.actionSystem.EditorActionHandler.execute(EditorActionHandler.java:189)
at com.intellij.openapi.editor.actions.DeleteSelectionHandler.executeWriteAction(DeleteSelectionHandler.java:33)
at com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler$1.run(EditorWriteActionHandler.java:42)
at com.intellij.openapi.application.impl.ApplicationImpl.runWriteAction(ApplicationImpl.java:975)
at com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler.doExecute(EditorWriteActionHandler.java:56)
at com.intellij.openapi.editor.actionSystem.EditorActionHandler.lambda$execute$4(EditorActionHandler.java:199)
at com.intellij.openapi.editor.actionSystem.EditorActionHandler.doIfEnabled(EditorActionHandler.java:89)
at com.intellij.openapi.editor.actionSystem.EditorActionHandler.execute(EditorActionHandler.java:198)
at com.intellij.codeInsight.lookup.impl.BackspaceHandler.doExecute(BackspaceHandler.java:25)
at com.intellij.openapi.editor.actionSystem.DynamicEditorActionHandler.doExecute(DynamicEditorActionHandler.java:63)
at com.intellij.openapi.editor.actionSystem.EditorActionHandler.lambda$execute$4(EditorActionHandler.java:199)
at com.intellij.openapi.editor.actionSystem.EditorActionHandler.doIfEnabled(EditorActionHandler.java:89)
at com.intellij.openapi.editor.actionSystem.EditorActionHandler.execute(EditorActionHandler.java:198)
at com.intellij.openapi.editor.actionSystem.EditorAction.lambda$actionPerformed$0(EditorAction.java:92)
at com.intellij.openapi.command.impl.CoreCommandProcessor.executeCommand(CoreCommandProcessor.java:225)
at com.intellij.openapi.command.impl.CoreCommandProcessor.executeCommand(CoreCommandProcessor.java:177)
at com.intellij.openapi.editor.actionSystem.EditorAction.actionPerformed(EditorAction.java:101)
at com.intellij.openapi.editor.actionSystem.EditorAction.actionPerformed(EditorAction.java:77)
at com.intellij.openapi.actionSystem.ex.ActionUtil.doPerformActionOrShowPopup(ActionUtil.java:344)
at com.intellij.openapi.keymap.impl.ActionProcessor.performAction(ActionProcessor.java:32)
at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcher$myActionProcessor$1.performAction(IdeKeyEventDispatcher.kt:496)
at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcherKt.doPerformActionInner$lambda$4$lambda$3(IdeKeyEventDispatcher.kt:831)
at com.intellij.openapi.application.TransactionGuardImpl.performActivity(TransactionGuardImpl.java:106)
at com.intellij.openapi.application.TransactionGuardImpl.performUserActivity(TransactionGuardImpl.java:95)
at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcherKt.doPerformActionInner$lambda$4(IdeKeyEventDispatcher.kt:831)
at com.intellij.openapi.actionSystem.ex.ActionUtil.performDumbAwareWithCallbacks(ActionUtil.java:381)
at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcherKt.doPerformActionInner(IdeKeyEventDispatcher.kt:829)
at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcherKt.access$doPerformActionInner(IdeKeyEventDispatcher.kt:1)
at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcher.processAction$intellij_platform_ide_impl(IdeKeyEventDispatcher.kt:559)
at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcher.processAction(IdeKeyEventDispatcher.kt:509)
at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcher.processActionOrWaitSecondStroke(IdeKeyEventDispatcher.kt:448)
at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcher.inInitState(IdeKeyEventDispatcher.kt:441)
at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcher.dispatchKeyEvent(IdeKeyEventDispatcher.kt:303)
at com.intellij.ide.IdeEventQueue.dispatchKeyEvent(IdeEventQueue.kt:620)
at com.intellij.ide.IdeEventQueue._dispatchEvent$lambda$11(IdeEventQueue.kt:581)
at com.intellij.openapi.application.impl.RwLockHolder.runWithEnabledImplicitRead(RwLockHolder.kt:75)
at com.intellij.openapi.application.impl.RwLockHolder.runWithImplicitRead(RwLockHolder.kt:67)
at com.intellij.ide.IdeEventQueue._dispatchEvent(IdeEventQueue.kt:581)
at com.intellij.ide.IdeEventQueue.access$_dispatchEvent(IdeEventQueue.kt:72)
at com.intellij.ide.IdeEventQueue$dispatchEvent$processEventRunnable$1$1$1.compute(IdeEventQueue.kt:355)
at com.intellij.ide.IdeEventQueue$dispatchEvent$processEventRunnable$1$1$1.compute(IdeEventQueue.kt:354)
at com.intellij.openapi.progress.impl.CoreProgressManager.computePrioritized(CoreProgressManager.java:793)
at com.intellij.ide.IdeEventQueue$dispatchEvent$processEventRunnable$1$1.invoke(IdeEventQueue.kt:354)
at com.intellij.ide.IdeEventQueue$dispatchEvent$processEventRunnable$1$1.invoke(IdeEventQueue.kt:349)
at com.intellij.ide.IdeEventQueueKt.performActivity$lambda$1(IdeEventQueue.kt:1014)
at com.intellij.openapi.application.TransactionGuardImpl.performActivity(TransactionGuardImpl.java:114)
at com.intellij.ide.IdeEventQueueKt.performActivity(IdeEventQueue.kt:1014)
at com.intellij.ide.IdeEventQueue.dispatchEvent$lambda$7(IdeEventQueue.kt:349)
at com.intellij.openapi.application.impl.ApplicationImpl.runIntendedWriteActionOnCurrentThread(ApplicationImpl.java:848)
at com.intellij.ide.IdeEventQueue.dispatchEvent(IdeEventQueue.kt:391)
at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:207)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:128)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:117)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:113)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:105)
at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:92)
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.
hmmm I am not sure how to get a line separator from the IDEA API. I cannot use VirtualFile::getDetectedLineSeparator()
so I was guessing from system.
I will replace this with \n
and hope it resolves your exception :/
LOG.info("not a nix ind string") | ||
return this | ||
} | ||
val originalNode = astNode.firstChildNode.treeNext.firstChildNode as? LeafPsiElement |
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.
issue: This method doesn't seem to escape the input, and may therefore also remove existing escape sequences. For some reason, some lines were also duplicated for me after trying it with the following code:
pkgs.writeShellScript "my-script.sh" ''
first_of_array=''${ARRAY[0]}
from_nix=${lib.escapeShellArg someVar}
''
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.
this is a good catch. I was aware this method did not escape the input and would remove existing escape sequences. This is because at the stage where we get the bash text, I have no way to know if the coming ${
is a bash interpolation or a Nix one, so I don't know whether it should be escaped. Likewise for newlines: If I get a \n
char, should it get converted to an escaped ''\n
or into an actual \n
?
I think this is where the method of not injecting in the text fragment may have reached its limits :/
* | ||
* @returns the minIndent of the string if successful, or null if unsuccessful. | ||
*/ | ||
fun unescapeAndDecode(chars: String, outChars: StringBuilder, sourceOffsets: IntArray?): Boolean { |
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.
note: I am not entirely happy with having two implementations to parse/unescape strings now. Anyway, that is not important right now. I may merge them at some point in the future.
- fix outdated javadoc - fix outdated valid injection host check
Thanks for your review 🙏 Of course you are welcome to try anything you please! As for the escaping issue, you are correct that it's a blocker. I think I might have to re-implement this with injections directly in the text fragment, but I have to learn how it works for Java and Kotlin first. I do not see another way to get around the problem you pointed out. Note Kotlin (which has |
Just to give you some status update: I noticed that there were also some problems regarding the decoding of the strings. Most noticeably, the indentation is not detected correctly, and I think empty strings would cause issues. (I think the following assumption is not correct.) nix-idea/src/main/java/org/nixos/idea/psi/NixStringLiteralEscaper.kt Lines 75 to 77 in 7d2f79b
After my first attempt to detect the indentation correctly (af39db9, only detection of indent), I got the idea that I could adjust the lexer to make the implementation much easier. I have now implemented the correct removal of the indentation at f47a403. I also made a first attempt to use this decoding in I didn't yet manage to look into the main issue regarding the interpolations. I may also not have time for this topic for the second half of this week. :( If you like, I can push these changes on your branch. I wasn't sure if you would like that, as some of my changes are quite different compared to the code you have touched so far, and pushing them into the PR would mean that they would eventually end up in "your" commit after squashing the PR. |
hey @JojOatXGME ! sorry for the late replies. I will be a bit unresponsive the coming week but I am keeping an eye on this PR.
Feel free to push into mine, if we need my work we can always branch off at 7d2f79b later.
I had a lot of this in my initially. Over the coming week I will try to look at your new approach and see where the wrong offsets might be! At any rate thanks for your continued support |
While looking at this topic again, I noticed the following statement from the Javadoc of
I gave it a try and implemented Anyway, while looking into this topic, I also noticed further edge cases and bugs I tried to resolve. This is why the commit is much bigger. One remaining bug is that I get a strange See exception(Looks like the indent of the first line is missing in the PSI/AST after patching, whatever patching means in this case.)
I also noticed that JetBrains' documentation at https://plugins.jetbrains.com/docs/intellij/language-injection.html mentions that language plugins should implement |
Hey @JojOatXGME I am sorry for the late reply. First off, congrats on your progress with ce8ee54 it does mostly work. This afternoon I have tried to make sense of the problem you are seeing and implement |
No worries. I am also struggling to understand IDEA from time to time. You were already a great help. 👍 While I may have changed some parts of your initial implementation, we wouldn't have brought it that far without your initiative to create some prototype. Unfortunately, I didn't have time over the last two months. Not entirely sure how my next weeks are going to look, but I believe, we can make it work in the not so far future. 😄 FYI, my hypothesis regarding the exception is that IDEA is confused with the first ignored line of the string. The following code in the lexer tells IntelliJ that the first line is insignificant whitespace and can be ignored during parsing. nix-idea/src/main/lang/Nix.flex Lines 77 to 80 in ce8ee54
My best guess is that some part of IntelliJ is then implicitly truncating this insignificant whitespace during some internal processing, which then causes a different part of IntelliJ to fail, as it didn't expect that the whitespaces are missing now. At least this is the only idea I have right now which could explain the exception. Anyway, I would still favor to have some tests, even when we have resolved the exception. Also, if you have any question about the change I did, feel free to ask. After all, I changed some code you haven't touched in this PR before. |
Thanks! I agree and your hypothesis would make sense, but I don't understand the grammar enough to suggest a change that could test your theory. I also agree that proper tests would be ideal! I think the only question I have is around this test case (and similar ones) that fail (as of ce8ee54). I started looking into fixing them but I do not think I fully understand why you expect it to not have the leading whitespace in the test. |
I don't remember if I tried to implement this case yet. I primarily wrote down what I expect as a user in the tests. Adding leading whitespace to an empty line would be unnecessary, as there is nothing to indent. At the same time, this whitespace would also be trailing whitespace. I personally don't like trailing whitespace and try to avoid it. I don't know if my tendency to avoid trailing whitespace is just a personal thing, or a bigger convention. I only know that Git classifies them as “whitespace errors” by default, but that only means that they are highlighted in diffs. FYI: I think you should also be able to create review comments. This way, you can write your comments on the specific lines in the diff where you have the questions. You can also put your permanent code links into a separate line, GitHub will then actually inline a code snipped showing the code in your comment. (Only works if the link points to code in the same repository.) |
Implement
PsiLanguageInjectionHost
in strings, so that they may be injected with the one-off action "Inject Language Or Reference". Works towards closing #81.There are several not-yet-implemented things, some more important than others. In no particular order:
NixStringLiteralEscaper.getOffsetInHost()
is not implemented, because it's hard. This means the action "Edit Fragment" works but does not edit the fragment (it opens a separate window to edit the guest language). This also means guest languages cannot be formatted.Another potential issue is that I implemented much of the boilerplate in Kotlin, because I am more at ease writing in it. It's not super in line with the rest of the code, so I am happy to re-write in Java now that the actual development is done, if you prefer.
For reference, I used the Terraform HCL plugin as my guide, because it also has multiline strings with interpolation.
I am happy to keep working on this PR to implement the things above, or do separate PRs so they are smaller and easier to review. See some pictures below.
Some languages:
A warning hint of Yaml inside a Nix string:
The fragment editor to see the isolated guest language (this does not work if you edit the fragment, because
getOffsetInHost()
is not implemented):One line injection: