Skip to content

Commit 4d57c71

Browse files
fix: allow navigation to ranges in files which are not IDE documents [IDE-1781] (#793)
* fix: allow navigation to ranges in files which are not IDE documents * chore: improve logging * chore: lint
1 parent c54806f commit 4d57c71

File tree

2 files changed

+104
-5
lines changed

2 files changed

+104
-5
lines changed

src/main/kotlin/io/snyk/plugin/Utils.kt

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -352,10 +352,24 @@ fun navigateToSource(
352352
) {
353353
runAsync {
354354
if (project.isDisposed || !virtualFile.isValid) return@runAsync
355-
val textLength = virtualFile.getDocument()?.textLength ?: return@runAsync
356-
if (selectionStartOffset !in (0 until textLength)) {
357-
logger.warn("Navigation to wrong offset: $selectionStartOffset with file length=$textLength")
358-
return@runAsync
355+
// Check that the offset is valid. If not, log it and let PsiNavigationSupport handle it instead
356+
// of returning early
357+
// This can happen if the IDE does not consider the file to be a document (e.g.,
358+
// .gitleaksignore)
359+
val document = virtualFile.getDocument()
360+
361+
if (document == null) {
362+
logger.warn(
363+
"Cannot parse ${virtualFile.name} as a document; navigation to selection may fail"
364+
)
365+
}
366+
367+
val textLength = document?.textLength
368+
369+
if (textLength != null && selectionStartOffset !in (0 until textLength)) {
370+
logger.warn(
371+
"Navigation to wrong offset: $selectionStartOffset. ${virtualFile.name} file length=$textLength"
372+
)
359373
}
360374

361375
if (selectionStartOffset >= 0) {
@@ -373,7 +387,7 @@ fun navigateToSource(
373387
}
374388
}
375389

376-
if (selectionEndOffset != null) {
390+
if (document != null && selectionEndOffset != null) {
377391
// highlight(by selection) suggestion range in source file
378392
invokeLater {
379393
if (project.isDisposed) return@invokeLater

src/test/kotlin/io/snyk/plugin/UtilsKtTest.kt

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package io.snyk.plugin
22

3+
import com.intellij.ide.util.PsiNavigationSupport
34
import com.intellij.openapi.application.ApplicationManager
45
import com.intellij.openapi.project.Project
56
import com.intellij.openapi.vfs.VirtualFile
7+
import com.intellij.pom.Navigatable
68
import com.intellij.util.messages.MessageBus
79
import com.intellij.util.messages.Topic
810
import io.mockk.every
@@ -311,6 +313,89 @@ class UtilsKtTest {
311313
verify(exactly = 0) { messageBus.syncPublisher(any<Topic<TestListener>>()) }
312314
}
313315

316+
@Test
317+
fun `navigateToSource should still open file when getDocument returns null`() {
318+
unmockkAll()
319+
mockkStatic("io.snyk.plugin.UtilsKt")
320+
mockkStatic(ApplicationManager::class)
321+
mockkStatic(PsiNavigationSupport::class)
322+
323+
val appMock = mockk<com.intellij.openapi.application.Application>(relaxed = true)
324+
every { ApplicationManager.getApplication() } returns appMock
325+
every { appMock.isDisposed } returns false
326+
// Mock invokeLater to execute runnables immediately
327+
// The top-level invokeLater delegates to Application.invokeLater with ModalityState
328+
every { appMock.invokeLater(any<Runnable>()) } answers { firstArg<Runnable>().run() }
329+
every {
330+
appMock.invokeLater(any<Runnable>(), any<com.intellij.openapi.application.ModalityState>())
331+
} answers { firstArg<Runnable>().run() }
332+
333+
val project = mockk<Project>(relaxed = true)
334+
every { project.isDisposed } returns false
335+
336+
val virtualFile = mockk<VirtualFile>(relaxed = true)
337+
every { virtualFile.isValid } returns true
338+
// Simulate a file type that IntelliJ doesn't recognize (e.g., .gitleaksignore)
339+
every { virtualFile.getDocument() } returns null
340+
341+
val latch = CountDownLatch(1)
342+
val navigatable = mockk<Navigatable>(relaxed = true)
343+
every { navigatable.canNavigateToSource() } returns true
344+
every { navigatable.canNavigate() } returns true
345+
every { navigatable.navigate(any()) } answers { latch.countDown() }
346+
347+
val psiNavSupport = mockk<PsiNavigationSupport>(relaxed = true)
348+
every { PsiNavigationSupport.getInstance() } returns psiNavSupport
349+
every { psiNavSupport.createNavigatable(project, virtualFile, 5) } returns navigatable
350+
351+
navigateToSource(project, virtualFile, 5, 10)
352+
353+
assertTrue("Navigation should complete within timeout", latch.await(2, TimeUnit.SECONDS))
354+
verify { navigatable.navigate(false) }
355+
}
356+
357+
@Test
358+
fun `navigateToSource should still open file when offset is out of bounds`() {
359+
unmockkAll()
360+
mockkStatic("io.snyk.plugin.UtilsKt")
361+
mockkStatic(ApplicationManager::class)
362+
mockkStatic(PsiNavigationSupport::class)
363+
364+
val appMock = mockk<com.intellij.openapi.application.Application>(relaxed = true)
365+
every { ApplicationManager.getApplication() } returns appMock
366+
every { appMock.isDisposed } returns false
367+
every { appMock.invokeLater(any<Runnable>()) } answers { firstArg<Runnable>().run() }
368+
every {
369+
appMock.invokeLater(any<Runnable>(), any<com.intellij.openapi.application.ModalityState>())
370+
} answers { firstArg<Runnable>().run() }
371+
372+
val project = mockk<Project>(relaxed = true)
373+
every { project.isDisposed } returns false
374+
375+
val document = mockk<com.intellij.openapi.editor.Document>(relaxed = true)
376+
every { document.textLength } returns 50
377+
378+
val virtualFile = mockk<VirtualFile>(relaxed = true)
379+
every { virtualFile.isValid } returns true
380+
every { virtualFile.getDocument() } returns document
381+
382+
// Offset 100 is beyond document length of 50
383+
val latch = CountDownLatch(1)
384+
val navigatable = mockk<Navigatable>(relaxed = true)
385+
every { navigatable.canNavigateToSource() } returns true
386+
every { navigatable.canNavigate() } returns true
387+
every { navigatable.navigate(any()) } answers { latch.countDown() }
388+
389+
val psiNavSupport = mockk<PsiNavigationSupport>(relaxed = true)
390+
every { PsiNavigationSupport.getInstance() } returns psiNavSupport
391+
every { psiNavSupport.createNavigatable(project, virtualFile, 100) } returns navigatable
392+
393+
navigateToSource(project, virtualFile, 100, 110)
394+
395+
assertTrue("Navigation should complete within timeout", latch.await(2, TimeUnit.SECONDS))
396+
verify { navigatable.navigate(false) }
397+
}
398+
314399
interface TestListener {
315400
fun onEvent()
316401
}

0 commit comments

Comments
 (0)