Skip to content

Commit c211ce3

Browse files
authored
Merge pull request #5714 from ethereum/workspace_completion
Workspace completion
2 parents 97d08e9 + 24c93fd commit c211ce3

File tree

8 files changed

+197
-28
lines changed

8 files changed

+197
-28
lines changed

apps/remix-ide/src/app/plugins/remixAIPlugin.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import React, { useCallback } from 'react';
66
import { ICompletions, IModel, RemoteInferencer, IRemoteModel, IParams, GenerationParams, CodeExplainAgent, SecurityAgent } from '@remix/remix-ai-core';
77
import { CustomRemixApi } from '@remix-api'
88
import { PluginViewWrapper } from '@remix-ui/helper'
9+
import { CodeCompletionAgent } from '@remix/remix-ai-core';
910
const _paq = (window._paq = window._paq || [])
1011

1112
type chatRequestBufferT<T> = {
@@ -41,6 +42,7 @@ export class RemixAIPlugin extends ViewPlugin {
4142
securityAgent: SecurityAgent
4243
useRemoteInferencer:boolean = false
4344
dispatch: any
45+
completionAgent: CodeCompletionAgent
4446

4547
constructor(inDesktop:boolean) {
4648
super(profile)
@@ -62,7 +64,7 @@ export class RemixAIPlugin extends ViewPlugin {
6264
this.useRemoteInferencer = true
6365
this.initialize()
6466
}
65-
67+
this.completionAgent = new CodeCompletionAgent(this)
6668
this.securityAgent = new SecurityAgent(this)
6769
}
6870

@@ -107,10 +109,14 @@ export class RemixAIPlugin extends ViewPlugin {
107109
}
108110

109111
async code_completion(prompt: string, promptAfter: string): Promise<any> {
112+
if (this.completionAgent.indexer == null || this.completionAgent.indexer == undefined) await this.completionAgent.indexWorkspace()
113+
114+
const currentFile = await this.call('fileManager', 'getCurrentFile')
115+
const contextfiles = await this.completionAgent.getContextFiles(prompt)
110116
if (this.isOnDesktop && !this.useRemoteInferencer) {
111117
return await this.call(this.remixDesktopPluginName, 'code_completion', prompt, promptAfter)
112118
} else {
113-
return await this.remoteInferencer.code_completion(prompt, promptAfter)
119+
return await this.remoteInferencer.code_completion(prompt, promptAfter, contextfiles, currentFile)
114120
}
115121
}
116122

@@ -169,10 +175,15 @@ export class RemixAIPlugin extends ViewPlugin {
169175
}
170176

171177
async code_insertion(msg_pfx: string, msg_sfx: string): Promise<any> {
178+
if (this.completionAgent.indexer == null || this.completionAgent.indexer == undefined) await this.completionAgent.indexWorkspace()
179+
180+
const currentFile = await this.call('fileManager', 'getCurrentFile')
181+
const contextfiles = await this.completionAgent.getContextFiles(msg_pfx)
182+
172183
if (this.isOnDesktop && !this.useRemoteInferencer) {
173184
return await this.call(this.remixDesktopPluginName, 'code_insertion', msg_pfx, msg_sfx)
174185
} else {
175-
return await this.remoteInferencer.code_insertion(msg_pfx, msg_sfx)
186+
return await this.remoteInferencer.code_insertion( msg_pfx, msg_sfx, contextfiles, currentFile)
176187
}
177188
}
178189

@@ -195,7 +206,7 @@ export class RemixAIPlugin extends ViewPlugin {
195206
else {
196207
console.log("chatRequestBuffer is not empty. First process the last request.", this.chatRequestBuffer)
197208
}
198-
_paq.push(['trackEvent', 'ai', 'remixAI_chat', 'askFromTerminal'])
209+
_paq.push(['trackEvent', 'ai', 'remixAI', 'remixAI_chat'])
199210
}
200211

201212
async ProcessChatRequestBuffer(params:IParams=GenerationParams){
Lines changed: 161 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,169 @@
1-
import * as fs from 'fs';
1+
import lunr from 'lunr';
22

3-
class CodeCompletionAgent {
4-
private codebase: string[];
3+
interface Document {
4+
id: number;
5+
filename: string;
6+
content: string;
7+
identifier: number;
8+
}
9+
10+
interface indexT{
11+
isIndexed: boolean;
12+
lastIndexedTime?: number;
13+
reason?: string;
14+
}
15+
16+
enum SupportedFileExtensions {
17+
solidity = '.sol',
18+
vyper = '.vy',
19+
circom = '.circom',
20+
}
21+
22+
export class CodeCompletionAgent {
23+
props: any;
24+
indexer: any;
25+
Documents: Document[] = [];
26+
INDEX_THRESHOLD = 0.1;
27+
N_MATCHES = 1;
28+
indexed: indexT = {
29+
isIndexed: false,
30+
lastIndexedTime: 0,
31+
reason: 'Init',
32+
};
33+
34+
constructor(props) {
35+
this.props = props;
36+
this.listenForChanges();
37+
this.indexer =lunr(function () {
38+
this.ref('id')
39+
this.field('filename')
40+
this.field('content')
41+
this.field('Identifier');
42+
});
43+
44+
setInterval(() => {
45+
this.indexWorkspace()
46+
}, 60000)
47+
}
48+
49+
listenForChanges() {
50+
this.props.on('fileManager', 'fileAdded', (path) => { this.indexed = { isIndexed: false, reason:"fileAdded" } });
51+
this.props.on('fileManager', 'fileRemoved', (path) => { this.indexed = { isIndexed: false, reason:"fileRemoved" } });
52+
this.props.on('filePanel', 'workspaceCreated', () => { this.indexed = { isIndexed: false, reason:"workspaceCreated" } });
53+
this.props.on('filePanel', 'workspaceRenamed', () => { this.indexed = { isIndexed: false, reason:"workspaceRenamed" }});
54+
this.props.on('filePanel', 'workspaceDeleted', () => { this.indexed = { isIndexed: false, reason:"workspaceDeleted" } });
55+
}
56+
57+
async getDcocuments() {
58+
try {
59+
const documents: Document[] = [];
60+
const jsonDirsContracts = await this.props.call('fileManager', 'copyFolderToJson', '/').then((res) => res.contracts);
61+
let c = 0;
62+
for (const file in jsonDirsContracts.children) {
63+
if (!Object.values(SupportedFileExtensions).some(ext => file.endsWith(ext))) continue;
64+
documents.push({
65+
id: ++c,
66+
filename: file,
67+
content: jsonDirsContracts.children[file].content,
68+
identifier: c - 1,
69+
});
70+
}
71+
return documents;
72+
} catch (error) {
73+
return [];
74+
}
75+
}
576

6-
constructor(codebasePath: string) {
7-
// git or fs
8-
this.codebase = this.loadCodebase(codebasePath);
77+
async getLocalImports(fileContent: string, currentFile: string) {
78+
try {
79+
const lines = fileContent.split('\n');
80+
const imports = [];
81+
82+
for (const line of lines) {
83+
const trimmedLine = line.trim();
84+
if (trimmedLine.startsWith('import')) {
85+
const parts = trimmedLine.split(' ');
86+
if (parts.length >= 2) {
87+
const importPath = parts[1].replace(/['";]/g, '');
88+
imports.push(importPath);
89+
}
90+
}
91+
}
92+
// Only local imports are those files that are in the workspace
93+
const localImports = this.Documents.length >0 ? imports.filter((imp) => {return this.Documents.find((doc) => doc.filename === imp);}) : [];
94+
95+
return localImports;
96+
} catch (error) {
97+
return [];
98+
}
999
}
10100

11-
private loadCodebase(path: string): string[] {
12-
const files = fs.readdirSync(path);
13-
return files
14-
.filter(file => file.endsWith('.ts'))
15-
.flatMap(file => fs.readFileSync(`${path}/${file}`, 'utf-8').split('\n'));
101+
indexWorkspace() {
102+
this.getDcocuments().then((documents) => {
103+
this.indexer =lunr(function () {
104+
this.ref('id')
105+
this.field('filename')
106+
this.field('content')
107+
this.field('Identifier');
108+
109+
documents.forEach(doc => {
110+
this.add(doc);
111+
});
112+
});
113+
this.Documents = documents;
114+
});
115+
116+
this.indexed = { isIndexed: true, lastIndexedTime: Date.now(), reason: 'init Indexing' };
16117
}
17118

18-
public getSuggestions(currentLine: string, numSuggestions: number = 3): string[] {
19-
const suggestions: string[] = [];
20-
// get `numSuggestions` from the llm
21-
return suggestions;
119+
async getContextFiles(prompt) {
120+
try {
121+
if (!this.indexed.isIndexed) {
122+
await this.indexWorkspace();
123+
}
124+
125+
const currentFile = await this.props.call('fileManager', 'getCurrentFile');
126+
const content = prompt;
127+
const searchResult = this.indexer.search(content)
128+
const fcps = await this.processResults(searchResult, currentFile);
129+
const resolvedFcps = await Promise.all(fcps);
130+
return resolvedFcps;
131+
} catch (error) {
132+
return [];
133+
}
22134
}
135+
136+
async processResults(results: any, currentFile: string) {
137+
138+
// remove the current file name from the results list
139+
const rmResults = await results.filter(result => {
140+
return this.Documents.find(doc => doc.id === Number(result.ref)).filename !== currentFile;
141+
});
142+
143+
// filter out the results which have the same extension as the current file.
144+
// Do not mix and match file extensions as this will lead to incorrect completions
145+
const extResults = await rmResults.filter(result => {
146+
return this.Documents.find(doc => doc.id === Number(result.ref)).filename.split('.').pop() === currentFile.split('.').pop();
147+
});
148+
149+
// filter out the results which have a score less than the INDEX_THRESHOLD
150+
const topResults = await extResults.filter(result => result.score >= this.INDEX_THRESHOLD).slice(0, this.N_MATCHES);
151+
152+
// get the LATEST content of the top results in case the file has been modified and not indexed yet
153+
const fileContentPairs = topResults.map(async result => {
154+
const document = this.Documents.find(doc => doc.id === Number(result.ref));
155+
const currentContent = await this.props.call('fileManager', 'readFile', document.filename);
156+
return { file: document.filename, content: currentContent };
157+
});
158+
159+
const localImports = await this.getLocalImports(await this.props.call('fileManager', 'readFile', currentFile), currentFile);
160+
// check if the local import is in fileContentPairs file
161+
for (const li of localImports) {
162+
if (fileContentPairs.find(fcp => fcp.file === li)) continue;
163+
const currentContent = await this.props.call('fileManager', 'readFile', li);
164+
fileContentPairs.push({ file: li, content: currentContent });
165+
}
166+
return fileContentPairs;
167+
}
168+
23169
}

libs/remix-ai-core/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ export {
2222
export * from './types/types'
2323
export * from './helpers/streamHandler'
2424
export * from './agents/codeExplainAgent'
25-
export * from './agents/securityAgent'
25+
export * from './agents/completionAgent'
26+
export * from './agents/securityAgent'

libs/remix-ai-core/src/inferencers/remote/remoteInference.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,15 @@ export class RemoteInferencer implements ICompletions {
110110
}
111111
}
112112

113-
async code_completion(prompt, promptAfter, options:IParams=CompletionParams): Promise<any> {
114-
const payload = { prompt, 'context':promptAfter, "endpoint":"code_completion", ...options }
113+
async code_completion(prompt, promptAfter, ctxFiles, fileName, options:IParams=CompletionParams): Promise<any> {
114+
const payload = { prompt, 'context':promptAfter, "endpoint":"code_completion",
115+
'ctxFiles':ctxFiles, 'currentFileName':fileName, ...options }
115116
return this._makeRequest(payload, AIRequestType.COMPLETION)
116117
}
117118

118-
async code_insertion(msg_pfx, msg_sfx, options:IParams=InsertionParams): Promise<any> {
119-
const payload = { "endpoint":"code_insertion", msg_pfx, msg_sfx, ...options, prompt: '' }
119+
async code_insertion(msg_pfx, msg_sfx, ctxFiles, fileName, options:IParams=InsertionParams): Promise<any> {
120+
const payload = { "endpoint":"code_insertion", msg_pfx, msg_sfx, 'ctxFiles':ctxFiles,
121+
'currentFileName':fileName, ...options, prompt: '' }
120122
return this._makeRequest(payload, AIRequestType.COMPLETION)
121123
}
122124

libs/remix-ai-core/src/types/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ export interface InferenceModel {
5050
}
5151

5252
export interface ICompletions{
53-
code_completion(context, params:IParams): Promise<any>;
54-
code_insertion(msg_pfx, msg_sfx, params:IParams): Promise<any>;
53+
code_completion(context, ctxFiles, fileName, params:IParams): Promise<any>;
54+
code_insertion(msg_pfx, msg_sfx, ctxFiles, fileName, params:IParams): Promise<any>;
5555
}
5656

5757
export interface IParams {

libs/remix-ui/editor/src/lib/providers/inlineCompletionProvider.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli
106106
const generatedText = output // no need to clean it. should already be
107107

108108
this.task = 'code_insertion'
109-
_paq.push(['trackEvent', 'ai', 'remixAI', this.task])
110109
const item: monacoTypes.languages.InlineCompletion = {
111110
insertText: generatedText,
112111
range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column)
@@ -163,7 +162,6 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli
163162
let clean = data
164163
// if clean starts with a comment, remove it
165164
if (clean.startsWith('//') || clean.startsWith('/*') || clean.startsWith('*') || clean.startsWith('*/')){
166-
console.log("clean starts with comment")
167165
return ""
168166
}
169167

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@
171171
"jszip": "^3.6.0",
172172
"just-once": "^2.2.0",
173173
"latest-version": "^5.1.0",
174+
"lunr": "^2.3.9",
174175
"merge": "^2.1.1",
175176
"npm-install-version": "^6.0.2",
176177
"octokit": "^3.1.2",

yarn.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16080,6 +16080,11 @@ flatted@^3.1.0:
1608016080
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
1608116081
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
1608216082

16083+
flexsearch@^0.7.43:
16084+
version "0.7.43"
16085+
resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.7.43.tgz#34f89b36278a466ce379c5bf6fb341965ed3f16c"
16086+
integrity sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg==
16087+
1608316088
flora-colossus@^1.0.0:
1608416089
version "1.0.1"
1608516090
resolved "https://registry.yarnpkg.com/flora-colossus/-/flora-colossus-1.0.1.tgz#aba198425a8185341e64f9d2a6a96fd9a3cbdb93"
@@ -21348,6 +21353,11 @@ lru_map@^0.3.3:
2134821353
resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd"
2134921354
integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==
2135021355

21356+
lunr@^2.3.9:
21357+
version "2.3.9"
21358+
resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1"
21359+
integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==
21360+
2135121361
lz-string@^1.4.4:
2135221362
version "1.4.4"
2135321363
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"

0 commit comments

Comments
 (0)