Skip to content

Commit

Permalink
feat: support code actions for refactoring sections
Browse files Browse the repository at this point in the history
  • Loading branch information
Leon committed Jan 26, 2024
1 parent 61c7565 commit 90751da
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 26 deletions.
4 changes: 2 additions & 2 deletions .helix/languages.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ name = "typescript"
auto-format = true
indent = { tab-width = 2, unit = " " }
language-servers = [
"ts",
"gpt"
"ts",
"gpt"
]

6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ This was made to run with [Bun](https://bun.sh/), but you can also use a precomp
#### Without Bun

```bash
wget https://github.com/leona/helix-gpt/releases/download/0.8/helix-gpt-0.8-x86_64-linux.tar.gz \
wget https://github.com/leona/helix-gpt/releases/download/0.9/helix-gpt-0.9-x86_64-linux.tar.gz \
-O /tmp/helix-gpt.tar.gz \
&& tar -zxvf /tmp/helix-gpt.tar.gz \
&& mv helix-gpt-0.8-x86_64-linux /usr/bin/helix-gpt \
&& mv helix-gpt-0.9-x86_64-linux /usr/bin/helix-gpt \
&& chmod +x /usr/bin/helix-gpt
```

#### With Bun

```bash
wget https://github.com/leona/helix-gpt/releases/download/0.8/helix-gpt-0.8.js -O helix-gpt.js
wget https://github.com/leona/helix-gpt/releases/download/0.9/helix-gpt-0.9.js -O helix-gpt.js
```

### Configuration
Expand Down
73 changes: 72 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import Lsp from "./lsp"
import { getContent, log, debounce } from "./utils"
import { completion as completionHandler } from "./completions"
import { completion as completionHandler, chat as chatHandler } from "./completions"
import { commands } from "./constants"

const main = async () => {
const lsp = new Lsp.Service({
capabilities: {
codeActionProvider: true,
executeCommandProvider: {
commands: commands.map(i => i.key)
},
completionProvider: {
resolveProvider: false,
// somebody please tell me how to trigger newlines
Expand All @@ -16,6 +21,72 @@ const main = async () => {
}
})

lsp.on(Lsp.Event.ExecuteCommand, async ({ ctx, request }) => {
const { command } = request.params
const { range, query } = request.params.arguments[0]

ctx.sendDiagnostics([
{
message: `Executing ${command}...`,
range,
severity: Lsp.DiagnosticSeverity.Information
}
], 10000)

const content = ctx.getContentFromRange(range)

try {
var result = await chatHandler(query, content, ctx.currentUri as string, ctx.language as string)
} catch (e) {
log("chat failed", e.message)

return ctx.sendDiagnostics([{
message: e.message,
severity: Lsp.DiagnosticSeverity.Error,
range
}], 10000)
}

log("received chat result:", result)

ctx.send({
method: Lsp.Event.ApplyEdit,
id: request.id,
params: {
label: command,
edit: {
changes: {
[ctx.currentUri as string]: [{
range,
newText: result
}]
}
}
}
})

ctx.resetDiagnostics()
})

lsp.on(Lsp.Event.CodeAction, ({ ctx, request }) => {
ctx.send({
id: request.id,
result: commands.map(i => ({
title: i.label,
kind: "quickfix",
diagnostics: [],
command: {
title: i.label,
command: i.key,
arguments: [{
range: request.params.range,
query: i.query
}]
}
}))
})
})

lsp.on(Lsp.Event.Completion, async ({ ctx, request }) => {
const lastContentVersion = ctx.contentVersion

Expand Down
69 changes: 53 additions & 16 deletions src/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ import openai from "./openai"

let copilotToken: string

const extractCodeBlock = (text: string, language: string): string => {
const pattern = new RegExp(`\`\`\`${language}([\\s\\S]*?)\`\`\``, 'g');
let match;
const blocks: string[] = [];

while ((match = pattern.exec(text)) !== null) {
blocks.push(match[0]);
}

const result = blocks[0];
const lines = result.split('\n');
return lines.slice(3, lines.length - 1).join('\n') + "\n";
}

export const handlers = {
openai: async (contents: any, filepath: string, languageId: string, suggestions = 3) => {
const messages = [
Expand Down Expand Up @@ -100,8 +114,10 @@ export const handlers = {
throw e
}
},
copilotOld: async (contents: any, language: string, suggestions = 3) => {
// Leaving this here. Other copilot handler is how vscode does it.
}

export const chatHandlers = {
copilot: async (request: string, contents: string, filepath: string, language: string) => {
const parsedToken = parseQueryStringToken(copilotToken)

if (!parsedToken?.exp || parseInt(parsedToken.exp) <= currentUnixTimestamp()) {
Expand All @@ -125,24 +141,27 @@ export const handlers = {

const messages = [
{
role: "system",
content: config.copilotContext.replace("<languageId>", language) + "\n\n" + `End of file context:\n\n${contents.contentAfter}`
"content": `You are an AI programming assistant.\nWhen asked for your name, you must respond with \"GitHub Copilot\".\nFollow the user's requirements carefully & to the letter.\n- Each code block starts with \`\`\` and // FILEPATH.\n- You always answer with ${language} code.\n- When the user asks you to document something, you must answer in the form of a ${language} code block.\nYour expertise is strictly limited to software development topics.\nFor questions not related to software development, simply give a reminder that you are an AI programming assistant.\nKeep your answers short and impersonal.`,
"role": "system"
},
{
role: "user",
content: `Start of file context:\n\n${contents.contentBefore}`
"content": `I have the following code in the selection:\n\`\`\`${language}\n// FILEPATH: ${filepath.replace('file://', '')}\n${contents}`,
"role": "user"
},
{
"content": request,
"role": "user"
}
]

const body = {
model: config.copilotModel,
maxTokens: 8192,
maxRequestTokens: 6144,
maxResponseTokens: 2048,
baseTokensPerMessage: 4,
baseTokensPerName: -1,
baseTokensPerCompletion: 3,
n: suggestions,
intent: true,
max_tokens: 7909,
model: "gpt-3.5-turbo",
n: 1,
stream: false,
temperature: 0.1,
top_p: 1,
messages
}

Expand All @@ -161,23 +180,41 @@ export const handlers = {
"Accept": "*/*",
"Connection": "close"
}

try {
return await openai.standard(config.copilotEndpoint as string + "/chat/completions", headers, body)
const result = await openai.standard(config.copilotEndpoint as string + "/chat/completions", headers, body)
log("got copilot chat result:", result)
return extractCodeBlock(result, language)
} catch (e) {
log("copilot request failed: " + e.message)
throw e
}
}
}

export const chat = async (request: string, contents: string, filepath: string, language: string) => {
if (!chatHandlers[config.handler]) {
log("chat handler does not exist")
throw new Error(`chat handler: ${config.handler} does not exist`)
}

try {
log("running chat handler:", config.handler)
return await chatHandlers[config.handler](request, contents, filepath, language)
} catch (e) {
log("chat failed", e.message)
throw new Error("Chat failed: " + e.message)
}
}

export const completion = async (contents: any, language: string, suggestions = 3) => {
if (!handlers[config.handler]) {
log("completion handler does not exist")
throw new Error(`completion handler: ${config.handler} does not exist`)
}

try {
log("running handler:", config.handler)
log("running completion handler:", config.handler)
return uniqueStringArray(await handlers[config.handler](contents, language, suggestions))
} catch (e) {
log("completion failed", e.message)
Expand Down
17 changes: 17 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,20 @@ export const examples = [
}
]

export const commands = [
{
key: "generateDocs",
label: "Generate documentation",
query: "Add documentation to this code."
},
{
key: "improveCode",
label: "Improve code",
query: "Improve this code."
},
{
key: "refactorFromComment",
label: "Refactor code from a comment",
query: "Refactor this code based on the comment."
}
]
15 changes: 13 additions & 2 deletions src/lsp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ enum Event {
DidOpen = "textDocument/didOpen",
DidChange = "textDocument/didChange",
Completion = "textDocument/completion",
CodeAction = "textDocument/codeAction",
ApplyEdit = "workspace/applyEdit",
ExecuteCommand = "workspace/executeCommand",
Initialize = "initialize",
Shutdown = "shutdown",
Exit = "exit",
Expand Down Expand Up @@ -46,6 +49,7 @@ interface IService {
receiveLine(line: string): Promise<void>;
start(): Promise<void>;
send({ method, id, result, params }: { method?: Event, id?: number, result?: any, params?: any }): void;
getContentFromRange({ range }: { range: Range }): string;
}

type EventRequest = {
Expand All @@ -59,12 +63,13 @@ class Service {
currentUri?: string
contentVersion: number
language?: string
contents?: string
contents: string

constructor({ capabilities }) {
this.emitter = new EventEmitter()
this.capabilities = capabilities
this.contentVersion = 0
this.contents = ""
this.registerDefault()
}

Expand Down Expand Up @@ -100,6 +105,12 @@ class Service {
})
}

getContentFromRange(range: Range): string {
log("getting content from range", JSON.stringify(range), this.contents)
const { start, end } = range
return this.contents.split("\n").slice(start.line, end.line + 1).join("\n")
}

positionalUpdate(text: string, range: Range) {
const lines = this.contents.split("\n")
const start = range.start.line
Expand Down Expand Up @@ -190,7 +201,7 @@ class Service {

async receiveLine(line: string) {
try {
const request = JSON.parse(line.split('\r\n')[2])
const request = JSON.parse(line.split('\r\n')[2].split('Content-Length')[0])

if (![Event.DidChange, Event.DidOpen].includes(request.method)) {
log("received request:", JSON.stringify(request))
Expand Down
4 changes: 2 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ export const genHexStr = (length: number) => {
}

export const getContent = async (contents: string, line: number, column: number) => {
const lines = contents.split('\n').slice(0, line + 1)
const lines = contents?.split('\n').slice(0, line + 1)
lines[lines.length - 1] = lines[lines.length - 1].split('').slice(0, column).join('')
const lastLine = lines[lines.length - 1]
const contentBefore = lines.join('\n')
const contentAfter = contents.split('\n').slice(line + 1).join('\n')
const contentAfter = contents?.split('\n').slice(line + 1).join('\n')
const lastCharacter = contentBefore.slice(-1)
const templatedContent = `${contentBefore}<BEGIN_COMPLETION>\n${contentAfter}`
return { contentBefore, contentAfter, lastCharacter, templatedContent, lastLine }
Expand Down

0 comments on commit 90751da

Please sign in to comment.