Skip to content
This repository was archived by the owner on Oct 24, 2021. It is now read-only.

Commit 9dd3f0d

Browse files
committed
Added login button
1 parent c19aed5 commit 9dd3f0d

File tree

7 files changed

+165
-62
lines changed

7 files changed

+165
-62
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
@file:Suppress("EXPERIMENTAL_API_USAGE")
2+
3+
package dev.koding.copilot.auth
4+
5+
import com.google.gson.annotations.SerializedName
6+
import dev.koding.copilot.config.settings
7+
import dev.koding.copilot.httpClient
8+
import dev.koding.copilot.util.Notifications
9+
import io.ktor.client.request.*
10+
import io.ktor.http.*
11+
import kotlinx.coroutines.GlobalScope
12+
import kotlinx.coroutines.launch
13+
import java.awt.Desktop
14+
import java.net.URI
15+
import javax.swing.JOptionPane
16+
17+
const val authenticateUrl = "https://vscode-auth.github.com/authorize/" +
18+
"?callbackUri=vscode://vscode.github-authentication/did-authenticate" +
19+
"&scope=read:user" +
20+
"&responseType=code" +
21+
"&authServer=https://github.com"
22+
23+
data class GitHubTokenResponse(
24+
@SerializedName("access_token")
25+
val accessToken: String?,
26+
@SerializedName("token_type")
27+
val tokenType: String?,
28+
val scope: String?
29+
)
30+
31+
data class CopilotTokenResponse(
32+
val token: String,
33+
@SerializedName("expires_at")
34+
val expiry: Int
35+
)
36+
37+
private suspend fun getAuthToken(url: String): CopilotTokenResponse {
38+
val code = Url(url).parameters["code"] ?: error("Code not present")
39+
40+
val tokenResponse = httpClient.post<GitHubTokenResponse>("https://vscode-auth.github.com/token") {
41+
parameter("code", code)
42+
accept(ContentType.Application.Json)
43+
}
44+
45+
return httpClient.get("https://api.github.com/copilot_internal/token") {
46+
header("Authorization", "token ${tokenResponse.accessToken ?: error("Invalid GitHub token")}")
47+
}
48+
}
49+
50+
fun handleLogin(handler: (String) -> Unit = { Notifications.send("Login successful") }) {
51+
Desktop.getDesktop().browse(URI.create(authenticateUrl))
52+
val url = JOptionPane.showInputDialog("Enter the callback URL")
53+
54+
// TODO: Change from global scope
55+
GlobalScope.launch {
56+
val token = getAuthToken(url).token
57+
settings.token = token
58+
handler(token)
59+
}
60+
}

src/main/kotlin/dev/koding/copilot/completion/CopilotCompletionContributor.kt

Lines changed: 43 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
package dev.koding.copilot.completion
22

3-
import com.intellij.codeInsight.completion.*
3+
import com.intellij.codeInsight.completion.CompletionContributor
4+
import com.intellij.codeInsight.completion.CompletionParameters
5+
import com.intellij.codeInsight.completion.CompletionResultSet
6+
import com.intellij.codeInsight.completion.CompletionSorter
47
import com.intellij.codeInsight.lookup.LookupElementBuilder
5-
import com.intellij.notification.Notification
68
import com.intellij.notification.NotificationType
79
import com.intellij.openapi.progress.ProcessCanceledException
810
import com.intellij.openapi.progress.ProgressManager
911
import com.intellij.openapi.util.TextRange
12+
import dev.koding.copilot.auth.handleLogin
1013
import dev.koding.copilot.completion.api.CompletionRequest
1114
import dev.koding.copilot.completion.api.CompletionResponse
1215
import dev.koding.copilot.config.settings
1316
import dev.koding.copilot.copilotIcon
17+
import dev.koding.copilot.util.Notifications
1418
import io.ktor.client.features.*
1519
import kotlinx.coroutines.GlobalScope
1620
import kotlinx.coroutines.launch
@@ -19,95 +23,79 @@ import kotlin.math.max
1923

2024
class CopilotCompletionContributor : CompletionContributor() {
2125

22-
private var notified = false
23-
2426
override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) {
2527
if (parameters.isAutoPopup) return
2628

27-
if (settings.token == null) {
28-
if (notified) return
29-
@Suppress("DialogTitleCapitalization")
30-
Notification(
31-
"Error Report",
32-
"GitHub Copilot",
33-
"You have not set a token for GitHub Copilot.",
34-
NotificationType.ERROR
35-
).notify(parameters.editor.project)
36-
return run { notified = true }
37-
}
29+
if (settings.token?.isBlank() == true) return Notifications.send(
30+
"You have not set a token for GitHub Copilot.",
31+
type = NotificationType.ERROR,
32+
once = true,
33+
Notifications.NotificationAction("Login") { handleLogin() })
3834

35+
val (prefix, suffix) = parameters.prefixSuffix
3936
val prompt = """
4037
// Language: ${parameters.originalFile.language.displayName}
4138
// Path: ${parameters.originalFile.name}
4239
${parameters.prompt}
4340
""".trimIndent()
4441

45-
val (prefix, suffix) = parameters.prefixSuffix
42+
val matcher = result.prefixMatcher
43+
val set = result
44+
.withPrefixMatcher(CopilotPrefixMatcher(matcher.cloneWithPrefix(matcher.prefix)))
45+
.withRelevanceSorter(CompletionSorter.defaultSorter(parameters, matcher).weigh(CopilotWeigher()))
4646

47+
// TODO: Fix freeze
4748
var response: CompletionResponse? = null
49+
var errored = false
50+
4851
val job = GlobalScope.launch {
4952
try {
5053
response = CompletionRequest(prompt).send(settings.token!!)
5154
} catch (e: ClientRequestException) {
52-
if (!notified) {
53-
@Suppress("DialogTitleCapitalization")
54-
Notification(
55-
"Error Report",
56-
"GitHub Copilot",
57-
"Failed to fetch response. Is your copilot token valid?",
58-
NotificationType.ERROR
59-
).notify(parameters.editor.project)
60-
notified = true
61-
}
62-
63-
return@launch result.stopHere()
55+
errored = true
56+
return@launch Notifications.send(
57+
"Failed to fetch response. Is your copilot token valid?",
58+
type = NotificationType.ERROR,
59+
once = true,
60+
Notifications.NotificationAction("Login") { handleLogin() })
6461
}
6562
}
6663

67-
if (result.isStopped) return
6864
while (response == null) {
65+
if (errored) return
6966
try {
7067
ProgressManager.getInstance().progressIndicator.checkCanceled()
7168
Thread.sleep(10)
7269
} catch (e: ProcessCanceledException) {
7370
job.cancel()
74-
return result.stopHere()
71+
return
7572
}
7673
}
7774

7875
val choices = response!!.choices.filter { it.text.isNotBlank() }
7976
if (choices.isEmpty()) return
8077

81-
val originalMatcher = result.prefixMatcher
82-
val set =
83-
result.withPrefixMatcher(CopilotPrefixMatcher(originalMatcher.cloneWithPrefix(originalMatcher.prefix)))
84-
.withRelevanceSorter(
85-
CompletionSorter.defaultSorter(parameters, originalMatcher)
86-
.weigh(CopilotWeigher())
87-
)
88-
8978
set.restartCompletionOnAnyPrefixChange()
9079
set.addAllElements(choices.map { choice ->
9180
val completion = choice.text.removePrefix(prefix.trim()).removeSuffix(suffix.trim())
9281
val insert = "$prefix${completion.trim()}\n"
9382

94-
PrioritizedLookupElement.withPriority(
95-
LookupElementBuilder.create(choice, "")
96-
.withInsertHandler { context, _ ->
97-
val caret = context.editor.caretModel
98-
val startOffset = caret.visualLineStart
99-
val endOffset = caret.visualLineEnd
100-
101-
context.document.deleteString(startOffset, endOffset)
102-
context.document.insertString(startOffset, insert)
103-
caret.moveToOffset(startOffset + insert.length - 1)
104-
}
105-
.withPresentableText(prefix.split(".").last())
106-
.withTailText(completion, true)
107-
.withTypeText("GitHub Copilot")
108-
.withIcon(copilotIcon)
109-
.bold(), Double.MAX_VALUE
110-
)
83+
LookupElementBuilder.create(choice, "")
84+
.withInsertHandler { context, _ ->
85+
val caret = context.editor.caretModel
86+
val startOffset = caret.visualLineStart
87+
val endOffset = caret.visualLineEnd
88+
89+
context.document.deleteString(startOffset, endOffset)
90+
context.document.insertString(startOffset, insert)
91+
caret.moveToOffset(startOffset + insert.length - 1)
92+
}
93+
.withPresentableText(prefix.split(".").last().trim())
94+
.withTailText(completion, true)
95+
.withCaseSensitivity(false)
96+
.withTypeText("GitHub Copilot")
97+
.withIcon(copilotIcon)
98+
.bold()
11199
})
112100
}
113101

src/main/kotlin/dev/koding/copilot/config/ApplicationConfigurable.form

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="dev.koding.copilot.config.ApplicationConfigurable">
3-
<grid id="27dc6" binding="panel" layout-manager="GridLayoutManager" row-count="3" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
3+
<grid id="27dc6" binding="panel" layout-manager="GridLayoutManager" row-count="3" column-count="5" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
44
<margin top="15" left="15" bottom="15" right="15"/>
55
<constraints>
66
<xy x="20" y="20" width="614" height="400"/>
@@ -19,13 +19,13 @@
1919
</component>
2020
<vspacer id="38819">
2121
<constraints>
22-
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
22+
<grid row="2" column="0" row-span="1" col-span="3" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
2323
</constraints>
2424
</vspacer>
2525
<grid id="351a0" layout-manager="GridLayoutManager" row-count="3" column-count="4" same-size-horizontally="false" same-size-vertically="false" hgap="5" vgap="5">
2626
<margin top="5" left="5" bottom="5" right="5"/>
2727
<constraints>
28-
<grid row="1" column="0" row-span="1" col-span="3" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
28+
<grid row="1" column="0" row-span="1" col-span="5" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
2929
</constraints>
3030
<properties/>
3131
<border type="none"/>
@@ -72,8 +72,21 @@
7272
</constraints>
7373
<properties/>
7474
</component>
75+
<component id="8ef0" class="javax.swing.JButton" binding="loginButton">
76+
<constraints>
77+
<grid row="0" column="3" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
78+
</constraints>
79+
<properties>
80+
<text value="Login"/>
81+
</properties>
82+
</component>
7583
</children>
7684
</grid>
85+
<hspacer id="58992">
86+
<constraints>
87+
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
88+
</constraints>
89+
</hspacer>
7790
</children>
7891
</grid>
7992
</form>

src/main/kotlin/dev/koding/copilot/config/ApplicationConfigurable.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package dev.koding.copilot.config
22

33
import com.intellij.openapi.options.Configurable
4+
import dev.koding.copilot.auth.handleLogin
45
import java.text.NumberFormat
6+
import javax.swing.JButton
57
import javax.swing.JFormattedTextField
68
import javax.swing.JPanel
79
import javax.swing.JTextField
@@ -13,6 +15,7 @@ class ApplicationConfigurable : Configurable {
1315
private lateinit var panel: JPanel
1416
private lateinit var copilotToken: JTextField
1517
private lateinit var contextLines: JFormattedTextField
18+
private lateinit var loginButton: JButton
1619

1720
override fun apply() {
1821
settings.token = copilotToken.text
@@ -21,6 +24,7 @@ class ApplicationConfigurable : Configurable {
2124

2225
override fun createComponent(): JPanel {
2326
contextLines.formatterFactory = DefaultFormatterFactory(NumberFormatter(NumberFormat.getIntegerInstance()))
27+
loginButton.addActionListener { handleLogin { copilotToken.text = it } }
2428
return panel
2529
}
2630

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package dev.koding.copilot.util
2+
3+
import com.intellij.notification.NotificationGroupManager
4+
import com.intellij.notification.NotificationType
5+
import com.intellij.openapi.actionSystem.AnAction
6+
import com.intellij.openapi.actionSystem.AnActionEvent
7+
8+
object Notifications {
9+
10+
private val group = NotificationGroupManager.getInstance().getNotificationGroup("GitHub Copilot")
11+
private var shown = mutableSetOf<String>()
12+
13+
@Suppress("DialogTitleCapitalization")
14+
fun send(
15+
message: String,
16+
type: NotificationType = NotificationType.INFORMATION,
17+
once: Boolean = false,
18+
vararg actions: NotificationAction
19+
) {
20+
if (once && message in shown) return
21+
group.createNotification(message, type)
22+
.apply {
23+
actions.forEach {
24+
addAction(object : AnAction(it.name) {
25+
override fun actionPerformed(e: AnActionEvent) = it.action()
26+
})
27+
}
28+
shown += message
29+
}.notify(null)
30+
}
31+
32+
data class NotificationAction(
33+
val name: String,
34+
val action: () -> Unit
35+
)
36+
}

src/main/resources/META-INF/plugin.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
id="Settings.Copilot"
1818
groupId="tools"
1919
instance="dev.koding.copilot.config.ApplicationConfigurable"/>
20-
20+
2121
<completion.contributor language="any"
2222
implementationClass="dev.koding.copilot.completion.CopilotCompletionContributor"
2323
order="first"/>
24+
25+
<notificationGroup id="GitHub Copilot" displayType="STICKY_BALLOON"/>
2426
</extensions>
2527
</idea-plugin>

0 commit comments

Comments
 (0)