@@ -7,12 +7,7 @@ import { ContextUpdateStrategy, Client as ServerClient } from '@proj-airi/server
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
export class Client {
|
||||
private client: null | ServerClient<Events> = null
|
||||
|
||||
async appendContext(context: string): Promise<void> {
|
||||
const id = nanoid()
|
||||
this.send({ data: { contextId: id, id, strategy: ContextUpdateStrategy.AppendSelf, text: context }, type: 'context:update' })
|
||||
}
|
||||
private client: ServerClient<Events> | null = null
|
||||
|
||||
async connect(): Promise<boolean> {
|
||||
try {
|
||||
@@ -35,15 +30,6 @@ export class Client {
|
||||
}
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return !!this.client
|
||||
}
|
||||
|
||||
async replaceContext(context: string): Promise<void> {
|
||||
const id = nanoid()
|
||||
this.send({ data: { contextId: id, id, strategy: ContextUpdateStrategy.ReplaceSelf, text: context }, type: 'context:update' })
|
||||
}
|
||||
|
||||
private async send(event: WebSocketEventOptionalSource<Events>): Promise<void> {
|
||||
if (!this.client) {
|
||||
useLogger().warn('Cannot send event: not connected to AIRI Server Channel')
|
||||
@@ -58,4 +44,18 @@ export class Client {
|
||||
useLogger().errorWithError('Failed to send event to AIRI:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async replaceContext(context: string): Promise<void> {
|
||||
const id = nanoid()
|
||||
this.send({ type: 'context:update', data: { strategy: ContextUpdateStrategy.ReplaceSelf, text: context, id, contextId: id } })
|
||||
}
|
||||
|
||||
async appendContext(context: string): Promise<void> {
|
||||
const id = nanoid()
|
||||
this.send({ type: 'context:update', data: { strategy: ContextUpdateStrategy.AppendSelf, text: context, id, contextId: id } })
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return !!this.client
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,31 +22,31 @@ export class ContextCollector {
|
||||
|
||||
// File information
|
||||
const file = {
|
||||
fileName: document.fileName,
|
||||
languageId: document.languageId,
|
||||
path: document.uri.fsPath,
|
||||
languageId: document.languageId,
|
||||
fileName: document.fileName,
|
||||
workspaceFolder: this.getWorkspaceFolder(document.uri),
|
||||
}
|
||||
|
||||
// Cursor position
|
||||
const cursor = {
|
||||
character: position.character,
|
||||
line: position.line,
|
||||
character: position.character,
|
||||
}
|
||||
|
||||
// Selected text
|
||||
const selection = editor.selection.isEmpty
|
||||
? undefined
|
||||
: {
|
||||
end: {
|
||||
character: editor.selection.end.character,
|
||||
line: editor.selection.end.line,
|
||||
},
|
||||
start: {
|
||||
character: editor.selection.start.character,
|
||||
line: editor.selection.start.line,
|
||||
},
|
||||
text: document.getText(editor.selection),
|
||||
start: {
|
||||
line: editor.selection.start.line,
|
||||
character: editor.selection.start.character,
|
||||
},
|
||||
end: {
|
||||
line: editor.selection.end.line,
|
||||
character: editor.selection.end.character,
|
||||
},
|
||||
}
|
||||
|
||||
// Current line
|
||||
@@ -62,12 +62,12 @@ export class ContextCollector {
|
||||
const git = await this.getGitInfo(document.uri)
|
||||
|
||||
return {
|
||||
context,
|
||||
currentLine,
|
||||
cursor,
|
||||
file,
|
||||
git,
|
||||
cursor,
|
||||
selection,
|
||||
currentLine,
|
||||
context,
|
||||
git,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
}
|
||||
@@ -96,13 +96,21 @@ export class ContextCollector {
|
||||
after.push(document.lineAt(i).text)
|
||||
}
|
||||
|
||||
return { after, before }
|
||||
return { before, after }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workspace folder path
|
||||
*/
|
||||
private getWorkspaceFolder(uri: vscode.Uri): string | undefined {
|
||||
const folder = vscode.workspace.getWorkspaceFolder(uri)
|
||||
return folder?.uri.fsPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Git information (simplified)
|
||||
*/
|
||||
private async getGitInfo(uri: vscode.Uri): Promise<undefined | { branch: string, isDirty: boolean }> {
|
||||
private async getGitInfo(uri: vscode.Uri): Promise<{ branch: string, isDirty: boolean } | undefined> {
|
||||
try {
|
||||
const gitExtension = vscode.extensions.getExtension('vscode.git')?.exports
|
||||
if (!gitExtension)
|
||||
@@ -122,12 +130,4 @@ export class ContextCollector {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workspace folder path
|
||||
*/
|
||||
private getWorkspaceFolder(uri: vscode.Uri): string | undefined {
|
||||
const folder = vscode.workspace.getWorkspaceFolder(uri)
|
||||
return folder?.uri.fsPath
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,44 +50,82 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
})
|
||||
|
||||
const extension = injeca.provide('extension', {
|
||||
dependsOn: { client, vscodeContext, contextCollector, eventListeners, lifecycle, controlLoopInterval },
|
||||
build: ({ dependsOn }) => setup({ ...dependsOn, isEnabled, sendInterval }),
|
||||
dependsOn: { client, contextCollector, controlLoopInterval, eventListeners, lifecycle, vscodeContext },
|
||||
})
|
||||
|
||||
injeca.invoke({
|
||||
callback: noop,
|
||||
dependsOn: { extension },
|
||||
callback: noop,
|
||||
})
|
||||
|
||||
await injeca.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate the plugin
|
||||
*/
|
||||
export async function deactivate() {
|
||||
const { client } = await injeca.resolve({ client: { key: 'proj-airi:client' } as unknown as ProvidedBy<Client> })
|
||||
const { eventListeners } = await injeca.resolve({ eventListeners: { key: 'self:event-listeners' } as unknown as ProvidedBy<vscode.Disposable[]> })
|
||||
const { controlLoopInterval } = await injeca.resolve({ controlLoopInterval: { key: 'self:control-loop:interval:send' } as unknown as ProvidedBy<IntervalHandle> })
|
||||
async function setup(params: {
|
||||
client: Client
|
||||
vscodeContext: vscode.ExtensionContext
|
||||
contextCollector: ContextCollector
|
||||
eventListeners: vscode.Disposable[]
|
||||
lifecycle: Lifecycle
|
||||
controlLoopInterval: IntervalHandle
|
||||
isEnabled: boolean
|
||||
sendInterval: number
|
||||
}) {
|
||||
// Connect to Airi Channel Server
|
||||
if (params.isEnabled) {
|
||||
const connected = await params.client.connect()
|
||||
if (connected) {
|
||||
window.showInformationMessage('AIRI Server Channel connected!')
|
||||
}
|
||||
else {
|
||||
window.showWarningMessage('AIRI Server Channel connection failed!')
|
||||
}
|
||||
}
|
||||
|
||||
unregisterListeners({ controlLoopInterval, eventListeners })
|
||||
client?.disconnect()
|
||||
useLogger().log('AIRI deactivated!')
|
||||
// Register commands
|
||||
params.vscodeContext.subscriptions.push(
|
||||
commands.registerCommand('airi-vscode.enable', async () => {
|
||||
params.isEnabled = true
|
||||
await params.client.connect()
|
||||
await registerListeners({ ...params })
|
||||
window.showInformationMessage('AIRI enabled!')
|
||||
}),
|
||||
|
||||
commands.registerCommand('airi-vscode.disable', () => {
|
||||
params.isEnabled = false
|
||||
unregisterListeners({ eventListeners: params.eventListeners, controlLoopInterval: params.controlLoopInterval })
|
||||
params.client.disconnect()
|
||||
window.showInformationMessage('AIRI disabled!')
|
||||
}),
|
||||
|
||||
commands.registerCommand('airi-vscode.status', () => {
|
||||
const status = params.isEnabled && params.client ? 'Connected' : 'Disconnected'
|
||||
window.showInformationMessage(`AIRI Server Channel status: ${status}.`)
|
||||
}),
|
||||
)
|
||||
|
||||
// Register event listeners if enabled
|
||||
if (params.isEnabled) {
|
||||
await registerListeners({ ...params })
|
||||
}
|
||||
|
||||
useLogger().log('AIRI activated successfully')
|
||||
}
|
||||
|
||||
/**
|
||||
* Register event listeners for file save and editor switch
|
||||
*/
|
||||
async function registerListeners(params: {
|
||||
client: Client
|
||||
contextCollector: ContextCollector
|
||||
controlLoopInterval: IntervalHandle
|
||||
eventListeners: vscode.Disposable[]
|
||||
isEnabled: boolean
|
||||
lifecycle: Lifecycle
|
||||
eventListeners: vscode.Disposable[]
|
||||
client: Client
|
||||
controlLoopInterval: IntervalHandle
|
||||
isEnabled: boolean
|
||||
sendInterval: number
|
||||
}) {
|
||||
unregisterListeners({ controlLoopInterval: params.controlLoopInterval, eventListeners: params.eventListeners })
|
||||
unregisterListeners({ eventListeners: params.eventListeners, controlLoopInterval: params.controlLoopInterval })
|
||||
|
||||
// File save event
|
||||
params.eventListeners.push(
|
||||
@@ -137,66 +175,24 @@ async function registerListeners(params: {
|
||||
}
|
||||
}
|
||||
|
||||
async function setup(params: {
|
||||
client: Client
|
||||
contextCollector: ContextCollector
|
||||
controlLoopInterval: IntervalHandle
|
||||
eventListeners: vscode.Disposable[]
|
||||
isEnabled: boolean
|
||||
lifecycle: Lifecycle
|
||||
sendInterval: number
|
||||
vscodeContext: vscode.ExtensionContext
|
||||
}) {
|
||||
// Connect to Airi Channel Server
|
||||
if (params.isEnabled) {
|
||||
const connected = await params.client.connect()
|
||||
if (connected) {
|
||||
window.showInformationMessage('AIRI Server Channel connected!')
|
||||
}
|
||||
else {
|
||||
window.showWarningMessage('AIRI Server Channel connection failed!')
|
||||
}
|
||||
}
|
||||
|
||||
// Register commands
|
||||
params.vscodeContext.subscriptions.push(
|
||||
commands.registerCommand('airi-vscode.enable', async () => {
|
||||
params.isEnabled = true
|
||||
await params.client.connect()
|
||||
await registerListeners({ ...params })
|
||||
window.showInformationMessage('AIRI enabled!')
|
||||
}),
|
||||
|
||||
commands.registerCommand('airi-vscode.disable', () => {
|
||||
params.isEnabled = false
|
||||
unregisterListeners({ controlLoopInterval: params.controlLoopInterval, eventListeners: params.eventListeners })
|
||||
params.client.disconnect()
|
||||
window.showInformationMessage('AIRI disabled!')
|
||||
}),
|
||||
|
||||
commands.registerCommand('airi-vscode.status', () => {
|
||||
const status = params.isEnabled && params.client ? 'Connected' : 'Disconnected'
|
||||
window.showInformationMessage(`AIRI Server Channel status: ${status}.`)
|
||||
}),
|
||||
)
|
||||
|
||||
// Register event listeners if enabled
|
||||
if (params.isEnabled) {
|
||||
await registerListeners({ ...params })
|
||||
}
|
||||
|
||||
useLogger().log('AIRI activated successfully')
|
||||
/**
|
||||
* Unregister all event listeners
|
||||
*/
|
||||
function unregisterListeners(params: { eventListeners: vscode.Disposable[], controlLoopInterval: IntervalHandle }) {
|
||||
params.eventListeners.forEach(listener => listener.dispose())
|
||||
params.eventListeners = []
|
||||
stopMonitoring({ controlLoopInterval: params.controlLoopInterval })
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitoring the coding context
|
||||
*/
|
||||
function startMonitoring(params: {
|
||||
client: Client
|
||||
contextCollector: ContextCollector
|
||||
lifecycle: Lifecycle
|
||||
client: Client
|
||||
controlLoopInterval: IntervalHandle
|
||||
isEnabled: boolean
|
||||
lifecycle: Lifecycle
|
||||
sendInterval: number
|
||||
}) {
|
||||
stopMonitoring({ controlLoopInterval: params.controlLoopInterval })
|
||||
@@ -233,10 +229,14 @@ function stopMonitoring(params: { controlLoopInterval: IntervalHandle }) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister all event listeners
|
||||
* Deactivate the plugin
|
||||
*/
|
||||
function unregisterListeners(params: { controlLoopInterval: IntervalHandle, eventListeners: vscode.Disposable[] }) {
|
||||
params.eventListeners.forEach(listener => listener.dispose())
|
||||
params.eventListeners = []
|
||||
stopMonitoring({ controlLoopInterval: params.controlLoopInterval })
|
||||
export async function deactivate() {
|
||||
const { client } = await injeca.resolve({ client: { key: 'proj-airi:client' } as unknown as ProvidedBy<Client> })
|
||||
const { eventListeners } = await injeca.resolve({ eventListeners: { key: 'self:event-listeners' } as unknown as ProvidedBy<vscode.Disposable[]> })
|
||||
const { controlLoopInterval } = await injeca.resolve({ controlLoopInterval: { key: 'self:control-loop:interval:send' } as unknown as ProvidedBy<IntervalHandle> })
|
||||
|
||||
unregisterListeners({ eventListeners, controlLoopInterval })
|
||||
client?.disconnect()
|
||||
useLogger().log('AIRI deactivated!')
|
||||
}
|
||||
|
||||
@@ -2,6 +2,44 @@
|
||||
* Coding context information
|
||||
*/
|
||||
export interface CodingContext {
|
||||
/**
|
||||
* File information
|
||||
*
|
||||
* @example {
|
||||
* "path": "/home/neko/Git/github.com/moeru-ai/airi/package.json",
|
||||
* "languageId": "json",
|
||||
* "fileName": "/home/neko/Git/github.com/moeru-ai/airi/package.json"
|
||||
* }
|
||||
*/
|
||||
file: {
|
||||
path: string
|
||||
languageId: string
|
||||
fileName: string
|
||||
workspaceFolder?: string
|
||||
}
|
||||
/**
|
||||
* Cursor position
|
||||
*
|
||||
* {
|
||||
* "line": 5,
|
||||
* "character": 35
|
||||
* }
|
||||
*/
|
||||
cursor: {
|
||||
line: number
|
||||
character: number
|
||||
}
|
||||
/** Selected text */
|
||||
selection?: {
|
||||
text: string
|
||||
start: { line: number, character: number }
|
||||
end: { line: number, character: number }
|
||||
}
|
||||
/** Current line */
|
||||
currentLine: {
|
||||
lineNumber: number
|
||||
text: string
|
||||
}
|
||||
/**
|
||||
* Context (previous and next N lines)
|
||||
*
|
||||
@@ -23,52 +61,14 @@ export interface CodingContext {
|
||||
* }
|
||||
*/
|
||||
context: {
|
||||
after: string[]
|
||||
before: string[]
|
||||
}
|
||||
/** Current line */
|
||||
currentLine: {
|
||||
lineNumber: number
|
||||
text: string
|
||||
}
|
||||
/**
|
||||
* Cursor position
|
||||
*
|
||||
* {
|
||||
* "line": 5,
|
||||
* "character": 35
|
||||
* }
|
||||
*/
|
||||
cursor: {
|
||||
character: number
|
||||
line: number
|
||||
}
|
||||
/**
|
||||
* File information
|
||||
*
|
||||
* @example {
|
||||
* "path": "/home/neko/Git/github.com/moeru-ai/airi/package.json",
|
||||
* "languageId": "json",
|
||||
* "fileName": "/home/neko/Git/github.com/moeru-ai/airi/package.json"
|
||||
* }
|
||||
*/
|
||||
file: {
|
||||
fileName: string
|
||||
languageId: string
|
||||
path: string
|
||||
workspaceFolder?: string
|
||||
after: string[]
|
||||
}
|
||||
/** Git information */
|
||||
git?: {
|
||||
branch: string
|
||||
isDirty: boolean
|
||||
}
|
||||
/** Selected text */
|
||||
selection?: {
|
||||
end: { character: number, line: number }
|
||||
start: { character: number, line: number }
|
||||
text: string
|
||||
}
|
||||
/**
|
||||
* Timestamp
|
||||
*
|
||||
@@ -81,6 +81,6 @@ export interface CodingContext {
|
||||
* Event types sent to Airi
|
||||
*/
|
||||
export interface Events {
|
||||
data: CodingContext
|
||||
type: 'coding:context' | 'coding:save' | 'coding:switch-file'
|
||||
data: CodingContext
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user