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

Add a service for checking Cody Ignore policy and decorate some UI with ignored status #1379

Merged
merged 5 commits into from
Apr 25, 2024
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
1 change: 1 addition & 0 deletions src/main/java/com/sourcegraph/Icons.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
public interface Icons {
Icon SourcegraphLogo = IconLoader.getIcon("/icons/sourcegraphLogo.svg", Icons.class);
Icon CodyLogo = IconLoader.getIcon("/icons/codyLogo.svg", Icons.class);
Icon CodyLogoSlash = IconLoader.getIcon("/icons/cody-logo-heavy-slash.svg", Icons.class);
Icon GearPlain = IconLoader.getIcon("/icons/gearPlain.svg", Icons.class);
Icon RepoHostBitbucket = IconLoader.getIcon("/icons/repo-host-bitbucket.svg", Icons.class);
Icon RepoHostGeneric = IconLoader.getIcon("/icons/repo-host-generic.svg", Icons.class);
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/sourcegraph/cody/Icons.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ interface StatusBar {
Icon CompletionInProgress = new AnimatedIcon.Default();
Icon CodyAvailable = IconLoader.getIcon("/icons/codyLogoMonochromatic.svg", Icons.class);
Icon CodyAutocompleteDisabled =
IconLoader.getIcon("/icons/codyLogoMonochromaticMuted.svg", Icons.class);
IconLoader.getIcon("/icons/cody-logo-heavy-slash.svg", Icons.class);

Icon CodyUnavailable =
IconLoader.getIcon("/icons/codyLogoMonochromaticUnavailable.svg", Icons.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.sourcegraph.cody.chat.AgentChatSessionService
import com.sourcegraph.cody.config.CodyApplicationSettings
import com.sourcegraph.cody.context.RemoteRepoSearcher
import com.sourcegraph.cody.edit.FixupService
import com.sourcegraph.cody.ignore.IgnoreOracle
import com.sourcegraph.cody.listeners.CodyFileEditorListener
import com.sourcegraph.cody.statusbar.CodyStatusService
import com.sourcegraph.utils.CodyEditorUtil
Expand Down Expand Up @@ -82,7 +83,7 @@ class CodyAgentService(project: Project) : Disposable {
}

agent.client.onIgnoreDidChange = Consumer {
println("handling ignore rules changing not yet implemented")
IgnoreOracle.getInstance(project).onIgnoreDidChange()
}

if (!project.isDisposed) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,13 @@ private constructor(
editor: Editor,
file: VirtualFile,
): ProtocolTextDocument {
val rfc3986Uri = Rfc3986UriEncoder.encode(file.url)
val text = FileDocumentManager.getInstance().getDocument(file)?.text
val selection = getSelection(editor)
return ProtocolTextDocument(rfc3986Uri, text, selection)
return ProtocolTextDocument(uriFor(file), text, selection)
}

fun uriFor(file: VirtualFile): String {
return Rfc3986UriEncoder.encode(file.url)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
package com.sourcegraph.cody.chat.actions

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.sourcegraph.cody.CodyToolWindowContent
import com.sourcegraph.cody.agent.protocol.ProtocolTextDocument
import com.sourcegraph.cody.chat.AgentChatSession
import com.sourcegraph.cody.commands.CommandId
import com.sourcegraph.cody.context.ui.ActionInIgnoredFileNotification
import com.sourcegraph.cody.ignore.IgnoreOracle
import com.sourcegraph.cody.ignore.IgnorePolicy

abstract class BaseCommandAction : BaseChatAction() {

abstract val myCommandId: CommandId

override fun doAction(project: Project) {
FileEditorManager.getInstance(project).selectedTextEditor?.let {
ApplicationManager.getApplication().assertIsDispatchThread()
FileEditorManager.getInstance(project).selectedTextEditor?.let { editor ->
val file = FileDocumentManager.getInstance().getFile(editor.document)
val protocolFile = file?.let { ProtocolTextDocument.fromVirtualFile(editor, it) } ?: return

// If this file is ignored, display an error and stop.
if (IgnoreOracle.getInstance(project).policyForUri(protocolFile.uri).get() !=
IgnorePolicy.USE) {
ActionInIgnoredFileNotification().notify(project)
return
}

CodyToolWindowContent.executeOnInstanceIfNotDisposed(project) {
switchToChatSession(AgentChatSession.createFromCommand(project, myCommandId))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.sourcegraph.cody.context.ui

import com.intellij.ide.BrowserUtil
import com.intellij.notification.Notification
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationType
import com.intellij.notification.impl.NotificationFullContent
import com.intellij.openapi.actionSystem.AnActionEvent
import com.sourcegraph.Icons
import com.sourcegraph.cody.ignore.CODY_IGNORE_DOCS_URL
import com.sourcegraph.common.CodyBundle
import com.sourcegraph.common.NotificationGroups

class ActionInIgnoredFileNotification :
Notification(
NotificationGroups.SOURCEGRAPH_ERRORS,
"",
CodyBundle.getString("ignore.action-in-ignored-file.detail"),
NotificationType.INFORMATION),
NotificationFullContent {

init {
icon = Icons.CodyLogoSlash

addAction(
object :
NotificationAction(
CodyBundle.getString("ignore.action-in-ignored-file.learn-more-cta")) {
override fun actionPerformed(event: AnActionEvent, notification: Notification) {
BrowserUtil.browse(CODY_IGNORE_DOCS_URL)
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.sourcegraph.cody.ignore

import com.intellij.ide.BrowserUtil
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.EditorNotificationProvider
import com.sourcegraph.Icons
import com.sourcegraph.cody.agent.protocol.ProtocolTextDocument
import java.util.function.Function
import javax.swing.JComponent

const val CODY_IGNORE_DOCS_URL = "https://sourcegraph.com/docs/cody/capabilities/ignore-context"

class IgnoreNotificationProvider : EditorNotificationProvider, DumbAware {
override fun collectNotificationData(
project: Project,
file: VirtualFile
): Function<in FileEditor, out JComponent?> {
val policy =
IgnoreOracle.getInstance(project).policyForUri(ProtocolTextDocument.uriFor(file)).get()
if (policy == IgnorePolicy.USE) {
return Function { null }
}
return Function {
EditorNotificationPanel(it).apply {
icon(Icons.CodyLogoSlash)
// TODO: This message is specific to the enterprise product and needs to be changed when we
// support cody ignore in the self-serve product
text = "Cody ignores this file because of your admin policy"

createActionLabel(
"Learn more", Runnable { BrowserUtil.browse(CODY_IGNORE_DOCS_URL) }, false)
}
}
}
}
88 changes: 88 additions & 0 deletions src/main/kotlin/com/sourcegraph/cody/ignore/IgnoreOracle.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.sourcegraph.cody.ignore

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.ui.EditorNotifications
import com.intellij.util.containers.SLRUMap
import com.sourcegraph.cody.agent.CodyAgentService
import com.sourcegraph.cody.agent.protocol.IgnoreTestParams
import com.sourcegraph.cody.statusbar.CodyStatusService
import java.util.concurrent.CompletableFuture

enum class IgnorePolicy(val value: String) {
IGNORE("ignore"),
USE("use"),
}

/**
* Provides details about whether files and repositories must be ignored as chat context, for
* autocomplete, etc. per policy.
*/
@Service(Service.Level.PROJECT)
class IgnoreOracle(private val project: Project) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmph.
rc24-duke-java-mascot

companion object {
fun getInstance(project: Project): IgnoreOracle {
return project.service<IgnoreOracle>()
}
}

private val cache = SLRUMap<String, IgnorePolicy>(100, 100)
@Volatile private var focusedPolicy: IgnorePolicy? = null
@Volatile private var willFocusUri: String? = null

val isEditingIgnoredFile: Boolean
get() {
return focusedPolicy == IgnorePolicy.IGNORE
}

fun focusedFileDidChange(uri: String) {
willFocusUri = uri
ApplicationManager.getApplication().executeOnPooledThread {
val policy = policyForUri(uri).get()
if (focusedPolicy != policy && willFocusUri == uri) {
focusedPolicy = policy
CodyStatusService.resetApplication(project)
}
}
}

/**
* Notifies the IgnoreOracle that the ignore policy has changed. Called by CodyAgentService's
* client callbacks.
*/
fun onIgnoreDidChange() {
synchronized(cache) { cache.clear() }

// Update editor notifications to refresh IgnoreNotificationProvider banners.
EditorNotifications.getInstance(project).updateAllNotifications()

// Re-set the focused file URI to update the status bar.
val uri = willFocusUri
if (uri != null) {
focusedFileDidChange(uri)
}
}

/** Gets whether `uri` should be ignored for autocomplete, context, etc. */
fun policyForUri(uri: String): CompletableFuture<IgnorePolicy> {
val completable = CompletableFuture<IgnorePolicy>()
val result = synchronized(cache) { cache[uri] }
if (result != null) {
completable.complete(result)
return completable
}
CodyAgentService.withAgent(project) { agent ->
val policy =
when (agent.server.ignoreTest(IgnoreTestParams(uri)).get().policy) {
"ignore" -> IgnorePolicy.IGNORE
"use" -> IgnorePolicy.USE
else -> throw IllegalStateException("invalid ignore policy value")
}
synchronized(cache) { cache.put(uri, policy) }
completable.complete(policy)
}
return completable
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.sourcegraph.cody.agent.CodyAgent
import com.sourcegraph.cody.agent.CodyAgentCodebase
import com.sourcegraph.cody.agent.CodyAgentService
import com.sourcegraph.cody.agent.protocol.ProtocolTextDocument
import com.sourcegraph.cody.ignore.IgnoreOracle

class CodyFocusChangeListener(val project: Project) : FocusChangeListener {

Expand All @@ -19,6 +20,7 @@ class CodyFocusChangeListener(val project: Project) : FocusChangeListener {
CodyAgentService.withAgent(project) { agent: CodyAgent ->
agent.server.textDocumentDidFocus(textDocument)
}
IgnoreOracle.getInstance(project).focusedFileDidChange(textDocument.uri)
}
}
}
6 changes: 6 additions & 0 deletions src/main/kotlin/com/sourcegraph/cody/statusbar/CodyStatus.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ enum class CodyStatus : PresentableEnum, WithIcon {

override val icon: Icon = Icons.StatusBar.CodyAutocompleteDisabled
},
InIgnoredFile {
override fun getPresentableText(): String =
"Cody autocomplete is disabled in this file because of your admin policy"

override val icon: Icon = Icons.StatusBar.CodyAutocompleteDisabled
},
CodyNotSignedIn {
override fun getPresentableText(): String = "No account signed-in"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.intellij.util.ui.UIUtil
import com.sourcegraph.cody.agent.CodyAgentService
import com.sourcegraph.cody.config.CodyAccountManager
import com.sourcegraph.cody.config.CodyAuthenticationManager
import com.sourcegraph.cody.ignore.IgnoreOracle
import com.sourcegraph.common.UpgradeToCodyProNotification
import com.sourcegraph.config.ConfigUtil
import javax.annotation.concurrent.GuardedBy
Expand Down Expand Up @@ -59,6 +60,8 @@ class CodyStatusService : CodyStatusListener, Disposable {
status =
if (!ConfigUtil.isCodyEnabled()) {
CodyStatus.CodyDisabled
} else if (IgnoreOracle.getInstance(project).isEditingIgnoredFile) {
CodyStatus.InIgnoredFile
} else if (!ConfigUtil.isCodyAutocompleteEnabled()) {
CodyStatus.AutocompleteDisabled
} else if (CodyAgentService.agentError.get() != null) {
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/CodyBundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,5 @@ gotit.autocomplete.message=This is how Cody displays autocomplete suggestions.<b

# Other Actions
action.cody.restartAgent.text=Restart Cody Agent
ignore.action-in-ignored-file.detail=Cody has ignored this file because of your Sourcegraph admin policy
ignore.action-in-ignored-file.learn-more-cta=Learn more
4 changes: 4 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

<incompatible-with>com.intellij.jetbrains.client</incompatible-with>

<depends>com.intellij.modules.lang</depends>
<depends>com.intellij.modules.platform</depends>
<depends optional="true" config-file="plugin-git.xml">Git4Idea</depends>
<depends optional="true" config-file="plugin-perforce.xml">
Expand Down Expand Up @@ -109,6 +110,9 @@
order="first, before commitCompletion"/>
<annotator language="SourcegraphRemoteRepoList"
implementationClass="com.sourcegraph.cody.context.RemoteRepoAnnotator"/>

<!-- Cody Ignore -->
<editorNotificationProvider implementation="com.sourcegraph.cody.ignore.IgnoreNotificationProvider" />
</extensions>

<applicationListeners>
Expand Down
7 changes: 7 additions & 0 deletions src/main/resources/icons/cody-logo-heavy-slash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/main/resources/icons/cody-logo-heavy-slash_dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading