Initial support for scratch files

This commit is contained in:
Natalia Selezneva
2017-12-27 11:52:53 +03:00
parent 82a997e0a2
commit 2ccf4b7d7f
15 changed files with 559 additions and 5 deletions
@@ -245,4 +245,8 @@ debugger.field.watchpoints.properties.panel.field.modification.label=Field &modi
debugger.field.watchpoints.properties.panel.field.initialization.label=Field &initialization
# Android Lint
android.klint.inspections.group.name=Android Lint for Kotlin
android.klint.inspections.group.name=Android Lint for Kotlin
# Scratch Files
scratch.run.button=Run Scratch File
scratch.clear.button=Clear results
@@ -0,0 +1,62 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.scratch
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.util.PsiUtil
import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils
import org.jetbrains.kotlin.idea.refactoring.getLineNumber
import org.jetbrains.kotlin.idea.refactoring.getLineStartOffset
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtImportDirective
import org.jetbrains.kotlin.psi.psiUtil.endOffset
class KtScratchFile(psiFile: KtFile) : ScratchFile(psiFile) {
override fun getExpressions(): List<ScratchExpression> {
// todo multiple expressions at one line
val doc = PsiDocumentManager.getInstance(psiFile.project).getDocument(psiFile) ?: return emptyList()
var line = 0
val result = arrayListOf<ScratchExpression>()
while (line < doc.lineCount) {
val start = psiFile.getLineStartOffset(line) ?: continue
val elementAtOffset = CodeInsightUtils.getTopmostElementAtOffset(
PsiUtil.getElementAtOffset(psiFile, start),
start,
KtImportDirective::class.java,
KtDeclaration::class.java
)
if (elementAtOffset is PsiWhiteSpace) {
line = doc.getLineNumber(elementAtOffset.endOffset) + 1
continue
}
if (elementAtOffset == null) {
line += 1
continue
}
result.add(ScratchExpression(elementAtOffset, elementAtOffset.getLineNumber(true), elementAtOffset.getLineNumber(false)))
line = elementAtOffset.getLineNumber(false) + 1
}
return result
}
}
@@ -0,0 +1,27 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.scratch
import com.intellij.psi.PsiFile
import org.jetbrains.kotlin.idea.scratch.repl.KtScratchReplExecutor
import org.jetbrains.kotlin.psi.KtFile
class KtScratchFileLanguageProvider: ScratchFileLanguageProvider() {
override fun createFile(psiFile: PsiFile): ScratchFile? {
return (psiFile as? KtFile)?.let { KtScratchFile(psiFile)}
}
}
@@ -0,0 +1,26 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.scratch
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
abstract class ScratchFile(val psiFile: PsiFile) {
abstract fun getExpressions(): List<ScratchExpression>
}
data class ScratchExpression(val element: PsiElement, val lineStart: Int, val lineEnd: Int = lineStart)
@@ -0,0 +1,44 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.scratch
import com.intellij.lang.Language
import com.intellij.lang.LanguageExtension
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.fileTypes.LanguageFileType
import com.intellij.psi.PsiFile
abstract class ScratchFileLanguageProvider {
abstract fun createFile(psiFile: PsiFile): ScratchFile?
companion object {
private val EXTENSION = LanguageExtension<ScratchFileLanguageProvider>("org.jetbrains.kotlin.scratchFileLanguageProvider")
fun get(language: Language): ScratchFileLanguageProvider? {
return ScratchFileLanguageProvider.EXTENSION.forLanguage(language)
}
fun get(fileType: FileType): ScratchFileLanguageProvider? {
return (fileType as? LanguageFileType)?.language?.let { get(it) }
}
fun createFile(psiFile: PsiFile): ScratchFile? {
return get(psiFile.language)?.createFile(psiFile)
}
}
}
@@ -0,0 +1,64 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.scratch
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.ide.scratch.ScratchFileService
import com.intellij.openapi.components.AbstractProjectComponent
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import org.jetbrains.kotlin.idea.caches.resolve.NotUnderContentRootModuleInfo
import org.jetbrains.kotlin.idea.caches.resolve.productionSourceInfo
import org.jetbrains.kotlin.idea.caches.resolve.testSourceInfo
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.moduleInfo
class ScratchFileModuleInfoProvider(project: Project) : AbstractProjectComponent(project) {
override fun projectOpened() {
myProject.messageBus.connect(myProject).subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, ScratchFileModuleListener())
}
private inner class ScratchFileModuleListener : FileEditorManagerListener {
override fun fileOpened(source: FileEditorManager, file: VirtualFile) {
if (!file.isValid) return
if (!ScratchFileService.isInScratchRoot(file)) return
val ktFile = PsiManager.getInstance(myProject).findFile(file) as? KtFile ?: return
getScratchPanel(ktFile)?.let { panel ->
ktFile.moduleInfo = getModuleInfo(panel.getModule())
panel.addModuleListener {
ktFile.moduleInfo = getModuleInfo(it)
// Drop caches for old module
ProjectRootManager.getInstance(myProject).incModificationCount()
// Force re-highlighting
DaemonCodeAnalyzer.getInstance(myProject).restart(ktFile)
}
}
}
}
private fun getModuleInfo(it: Module?) =
it?.testSourceInfo() ?: it?.productionSourceInfo() ?: NotUnderContentRootModuleInfo
}
@@ -0,0 +1,38 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.scratch.actions
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.scratch.ScratchFileLanguageProvider
import org.jetbrains.kotlin.idea.scratch.getScratchPanelFromSelectedEditor
class ClearScratchAction : AnAction(
KotlinBundle.message("scratch.clear.button"),
KotlinBundle.message("scratch.clear.button"),
AllIcons.Actions.GC
) {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val panel = getScratchPanelFromSelectedEditor(project) ?: return
val scratchFile = panel.scratchFile
ScratchFileLanguageProvider.get(scratchFile.psiFile.language)?.getOutputHandler()?.clear(scratchFile)
}
}
@@ -0,0 +1,59 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.scratch.actions
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.compiler.CompilerManager
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.scratch.ScratchFileLanguageProvider
import org.jetbrains.kotlin.idea.scratch.getScratchPanelFromSelectedEditor
class RunScratchAction : AnAction(
KotlinBundle.message("scratch.run.button"),
KotlinBundle.message("scratch.run.button"),
AllIcons.Actions.Execute
) {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val scratchTopPanel = getScratchPanelFromSelectedEditor(project) ?: return
val scratchFile = scratchTopPanel.scratchFile
val isMakeBeforeRun = false // todo use property from panel
val isRepl = true //todo use property from panel
val provider = ScratchFileLanguageProvider.get(scratchFile.psiFile.language) ?: return
val module = scratchTopPanel.getModule()
if (module == null) {
return
}
val runnable = r@ {
// todo
}
if (isMakeBeforeRun) {
CompilerManager.getInstance(project)
.make(module) { aborted, errors, _, _ -> if (!aborted && errors == 0) runnable() }
} else {
runnable()
}
}
}
@@ -0,0 +1,59 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.scratch
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import org.jetbrains.kotlin.idea.scratch.ui.ScratchTopPanel
import org.jetbrains.kotlin.psi.UserDataProperty
fun getEditorWithoutScratchPanel(project: Project, virtualFile: VirtualFile): TextEditor? {
val allTextEditors = getAllTextEditors(project, virtualFile)
for (editor in allTextEditors) {
if (editor.scratchTopPanel != null) return null
}
return allTextEditors.firstOrNull()
}
fun getEditorWithScratchPanel(project: Project, virtualFile: VirtualFile): Pair<TextEditor, ScratchTopPanel>? {
val firstWithPanel = getAllTextEditors(project, virtualFile).firstOrNull { it.scratchTopPanel != null }
return firstWithPanel?.let { firstWithPanel to firstWithPanel.scratchTopPanel!! }
}
fun getScratchPanel(psiFile: PsiFile): ScratchTopPanel? {
return getAllTextEditors(psiFile.project, psiFile.virtualFile).mapNotNull { it.scratchTopPanel }.firstOrNull()
}
fun getAllEditorsWithScratchPanel(project: Project): List<Pair<TextEditor, ScratchTopPanel>> =
FileEditorManager.getInstance(project).allEditors.filterIsInstance<TextEditor>().mapNotNull {
val panel = it.scratchTopPanel
if (panel != null) it to panel else null
}
fun getScratchPanelFromSelectedEditor(project: Project): ScratchTopPanel? =
FileEditorManager.getInstance(project).selectedEditors.asSequence()
.filterIsInstance<TextEditor>()
.mapNotNull { it.scratchTopPanel }
.firstOrNull()
private fun getAllTextEditors(project: Project, virtualFile: VirtualFile) =
FileEditorManager.getInstance(project).getAllEditors(virtualFile).filterIsInstance<TextEditor>().asSequence()
fun TextEditor.addScratchPanel(panel: ScratchTopPanel) {
scratchTopPanel = panel
FileEditorManager.getInstance(panel.scratchFile.psiFile.project).addTopComponent(this, panel)
}
fun TextEditor.removeScratchPanel(panel: ScratchTopPanel) {
scratchTopPanel = null
FileEditorManager.getInstance(panel.scratchFile.psiFile.project).removeTopComponent(this, panel)
}
private var TextEditor.scratchTopPanel: ScratchTopPanel? by UserDataProperty<TextEditor, ScratchTopPanel>(Key.create("scratch.panel"))
@@ -0,0 +1,63 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.scratch.ui
import com.intellij.ide.scratch.ScratchFileService
import com.intellij.openapi.components.AbstractProjectComponent
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import org.jetbrains.kotlin.idea.scratch.*
class ScratchFileHook(project: Project) : AbstractProjectComponent(project) {
override fun projectOpened() {
myProject.messageBus.connect(myProject).subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, ScratchEditorListener())
}
override fun projectClosed() {
getAllEditorsWithScratchPanel(myProject).forEach { (editor, panel) -> editor.removeScratchPanel(panel) }
}
private inner class ScratchEditorListener : FileEditorManagerListener {
private fun isPluggable(file: VirtualFile): Boolean {
if (!file.isValid) return false
if (!ScratchFileService.isInScratchRoot(file)) return false
val psiFile = PsiManager.getInstance(myProject).findFile(file) ?: return false
return ScratchFileLanguageProvider.get(psiFile.fileType) != null
}
override fun fileOpened(source: FileEditorManager, file: VirtualFile) {
if (!isPluggable(file)) return
val panel = ScratchTopPanel.createPanel(myProject, file) ?: return
getEditorWithoutScratchPanel(myProject, file)?.let { editor ->
editor.addScratchPanel(panel)
}
}
override fun fileClosed(source: FileEditorManager, file: VirtualFile) {
if (!isPluggable(file)) return
getEditorWithScratchPanel(myProject, file)?.let { (editor, panel) ->
editor.removeScratchPanel(panel)
}
}
}
}
@@ -0,0 +1,87 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.scratch.ui
import com.intellij.application.options.ModulesComboBox
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import com.intellij.ui.components.panels.HorizontalLayout
import com.intellij.uiDesigner.core.Spacer
import org.jetbrains.kotlin.idea.scratch.ScratchFile
import org.jetbrains.kotlin.idea.scratch.ScratchFileLanguageProvider
import org.jetbrains.kotlin.idea.scratch.actions.ClearScratchAction
import org.jetbrains.kotlin.idea.scratch.actions.RunScratchAction
import org.jetbrains.kotlin.idea.scratch.getScratchPanel
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JSeparator
val ScratchFile.scratchTopPanel: ScratchTopPanel?
get() = getScratchPanel(psiFile).firstOrNull { it.scratchFile == this@scratchTopPanel }
class ScratchTopPanel private constructor(val scratchFile: ScratchFile) : JPanel(HorizontalLayout(5)) {
companion object {
fun createPanel(project: Project, virtualFile: VirtualFile): ScratchTopPanel? {
val psiFile = PsiManager.getInstance(project).findFile(virtualFile) ?: return null
val scratchFile = ScratchFileLanguageProvider.createFile(psiFile) ?: return null
return ScratchTopPanel(scratchFile)
}
}
private val moduleChooser: ModulesComboBox
init {
add(createActionsToolbar())
add(JSeparator())
add(JLabel("Use classpath of module: "))
moduleChooser = createModuleChooser(scratchFile.psiFile.project)
add(moduleChooser)
add(Spacer())
}
fun getModule(): Module? = moduleChooser.selectedModule
fun addModuleListener(f: (Module) -> Unit) {
moduleChooser.addActionListener {
moduleChooser.selectedModule?.let { f(it) }
}
}
private fun createActionsToolbar(): JComponent {
val toolbarGroup = DefaultActionGroup().apply {
add(RunScratchAction())
addSeparator()
add(ClearScratchAction())
}
return ActionManager.getInstance().createActionToolbar(ActionPlaces.EDITOR_TOOLBAR, toolbarGroup, true).component
}
private fun createModuleChooser(project: Project): ModulesComboBox {
return ModulesComboBox().apply {
fillModules(project)
selectedIndex = 0
}
}
}
+7
View File
@@ -16,6 +16,11 @@
<extensionPoint qualifiedName="org.jetbrains.kotlin.highlighterExtension"
interface="org.jetbrains.kotlin.idea.highlighter.HighlighterExtension"/>
<extensionPoint name="scratchFileLanguageProvider" beanClass="com.intellij.lang.LanguageExtensionPoint">
<with attribute="implementationClass" implements="org.jetbrains.kotlin.idea.scratch.ScratchFileLanguageProvider"/>
</extensionPoint>
<extensionPoint qualifiedName="org.jetbrains.kotlin.binaryExtension"
interface="org.jetbrains.kotlin.idea.util.KotlinBinaryExtension"/>
<extensionPoint qualifiedName="org.jetbrains.kotlin.facetValidatorCreator"
@@ -48,5 +53,7 @@
<idePlatformSupport implementation="org.jetbrains.kotlin.caches.resolve.JvmPlatformSupport"/>
<idePlatformSupport implementation="org.jetbrains.kotlin.caches.resolve.JsPlatformSupport"/>
<idePlatformSupport implementation="org.jetbrains.kotlin.caches.resolve.CommonPlatformSupport"/>
<scratchFileLanguageProvider language="kotlin" implementationClass="org.jetbrains.kotlin.idea.scratch.KtScratchFileLanguageProvider"/>
</extensions>
</idea-plugin>
+6
View File
@@ -52,6 +52,12 @@
<interface-class>org.jetbrains.kotlin.idea.completion.CompletionBindingContextProvider</interface-class>
<implementation-class>org.jetbrains.kotlin.idea.completion.CompletionBindingContextProvider</implementation-class>
</component>
<component>
<implementation-class>org.jetbrains.kotlin.idea.scratch.ui.ScratchFileHook</implementation-class>
</component>
<component>
<implementation-class>org.jetbrains.kotlin.idea.scratch.ScratchFileModuleInfoProvider</implementation-class>
</component>
</project-components>
<application-components>
@@ -25,6 +25,7 @@ import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.refactoring.util.CommonRefactoringUtil;
import com.intellij.util.text.CharArrayUtil;
import kotlin.collections.ArraysKt;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.descriptors.ClassKind;
@@ -361,9 +362,9 @@ public class CodeInsightUtils {
@Nullable
public static <T> T getTopmostElementAtOffset(@NotNull PsiElement element, int offset, @NotNull Class<T> klass) {
public static <T> T getTopmostElementAtOffset(@NotNull PsiElement element, int offset, @NotNull Class<? extends T>... classes) {
T lastElementOfType = null;
if (klass.isInstance(element)) {
if (anyIsInstance(element, classes)) {
lastElementOfType = (T) element;
}
do {
@@ -371,7 +372,7 @@ public class CodeInsightUtils {
if (parent == null || (parent.getTextOffset() < offset) || parent instanceof KtBlockExpression) {
break;
}
if (klass.isInstance(parent)) {
if (anyIsInstance(parent, classes)) {
lastElementOfType = (T) parent;
}
element = parent;
@@ -380,4 +381,8 @@ public class CodeInsightUtils {
return lastElementOfType;
}
private static <T> boolean anyIsInstance(PsiElement finalElement, @NotNull Class<? extends T>[] klass) {
return ArraysKt.any(klass, aClass -> aClass.isInstance(finalElement));
}
}
@@ -321,7 +321,10 @@ fun PsiFile.getLineStartOffset(line: Int): Int? {
val startOffset = doc.getLineStartOffset(line)
val element = findElementAt(startOffset) ?: return startOffset
return PsiTreeUtil.skipSiblingsForward(element, PsiWhiteSpace::class.java, PsiComment::class.java)?.startOffset ?: startOffset
if (element is PsiWhiteSpace || element is PsiComment) {
return PsiTreeUtil.skipSiblingsForward(element, PsiWhiteSpace::class.java, PsiComment::class.java)?.startOffset ?: startOffset
}
return startOffset
}
return null