Skip to content

Commit 7d6a9ce

Browse files
committed
feat(mpp-vscode): add DevIns language support and status bar
Phase 5 - Status Bar: - Add StatusBarManager with idle/thinking/streaming/error states - Animated status icons during LLM operations - Click to open chat command Phase 6 - DevIns Language Support: - Add TextMate grammar for syntax highlighting - Support commands (/), agents (@), variables ($) - Add language configuration (brackets, folding, etc.) - Implement DevInsCompletionProvider with built-in completions - Register completion triggers for /, @, $ characters Testing: - Add unit tests for StatusBarManager - Add unit tests for DevInsCompletionProvider - All 56 tests passing Refs #31
1 parent 876e3a1 commit 7d6a9ce

File tree

10 files changed

+801
-5
lines changed

10 files changed

+801
-5
lines changed

mpp-vscode/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,12 @@ mpp-vscode/
8181
- [x] autodev.cancelDiff - 取消差异
8282
- [x] autodev.runAgent - 运行 Agent
8383
- [x] 快捷键绑定 (Cmd+Shift+A)
84-
- [ ] 状态栏集成
84+
- [x] 状态栏集成
8585

86-
### Phase 6: 高级功能
87-
- [ ] DevIns 语言支持
88-
- [ ] 语法高亮
89-
- [ ] 自动补全
86+
### Phase 6: 高级功能
87+
- [x] DevIns 语言支持
88+
- [x] 语法高亮 (TextMate grammar)
89+
- [x] 自动补全 (/, @, $ 触发)
9090
- [ ] 代码索引集成
9191
- [ ] 领域词典支持
9292
- [ ] React Webview UI (替换内嵌 HTML)

mpp-vscode/package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,25 @@
101101
"key": "ctrl+shift+a",
102102
"mac": "cmd+shift+a"
103103
}
104+
],
105+
"languages": [
106+
{
107+
"id": "DevIns",
108+
"aliases": ["devins", "devin"],
109+
"extensions": [".devins", ".devin"],
110+
"configuration": "syntaxes/language-configuration.json",
111+
"icon": {
112+
"light": "./resources/icon.svg",
113+
"dark": "./resources/icon.svg"
114+
}
115+
}
116+
],
117+
"grammars": [
118+
{
119+
"language": "DevIns",
120+
"scopeName": "source.devins",
121+
"path": "syntaxes/DevIns.tmLanguage.json"
122+
}
104123
]
105124
},
106125
"scripts": {

mpp-vscode/resources/icon.svg

Lines changed: 7 additions & 0 deletions
Loading

mpp-vscode/src/extension.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ import * as vscode from 'vscode';
88
import { IDEServer } from './services/ide-server';
99
import { DiffManager, DiffContentProvider } from './services/diff-manager';
1010
import { ChatViewProvider } from './providers/chat-view';
11+
import { StatusBarManager } from './services/status-bar';
12+
import { registerDevInsCompletionProvider } from './providers/devins-completion';
1113
import { createLogger } from './utils/logger';
1214

1315
export const DIFF_SCHEME = 'autodev-diff';
1416

1517
let ideServer: IDEServer | undefined;
18+
let statusBar: StatusBarManager | undefined;
1619
let logger: vscode.OutputChannel;
1720
let log: (message: string) => void = () => {};
1821

@@ -24,6 +27,10 @@ export async function activate(context: vscode.ExtensionContext) {
2427
log = createLogger(context, logger);
2528
log('AutoDev extension activated');
2629

30+
// Initialize Status Bar
31+
statusBar = new StatusBarManager();
32+
context.subscriptions.push({ dispose: () => statusBar?.dispose() });
33+
2734
// Initialize Diff Manager
2835
const diffContentProvider = new DiffContentProvider();
2936
const diffManager = new DiffManager(log, diffContentProvider);
@@ -113,6 +120,10 @@ export async function activate(context: vscode.ExtensionContext) {
113120
})
114121
);
115122

123+
// Register DevIns language completion provider
124+
context.subscriptions.push(registerDevInsCompletionProvider(context));
125+
log('DevIns language support registered');
126+
116127
// Show welcome message on first install
117128
const welcomeShownKey = 'autodev.welcomeShown';
118129
if (!context.globalState.get(welcomeShownKey)) {
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* DevIns Completion Provider - Auto-completion for DevIns language
3+
*/
4+
5+
import * as vscode from 'vscode';
6+
import { CompletionManager } from '../bridge/mpp-core';
7+
8+
/**
9+
* Built-in DevIns commands
10+
*/
11+
const BUILTIN_COMMANDS = [
12+
{ name: '/file', description: 'Read file content', args: ':path' },
13+
{ name: '/write', description: 'Write content to file', args: ':path' },
14+
{ name: '/run', description: 'Run shell command', args: ':command' },
15+
{ name: '/patch', description: 'Apply patch to file', args: ':path' },
16+
{ name: '/commit', description: 'Create git commit', args: ':message' },
17+
{ name: '/symbol', description: 'Find symbol in codebase', args: ':name' },
18+
{ name: '/rev', description: 'Review code changes', args: '' },
19+
{ name: '/refactor', description: 'Refactor code', args: ':instruction' },
20+
{ name: '/test', description: 'Generate tests', args: '' },
21+
{ name: '/doc', description: 'Generate documentation', args: '' },
22+
{ name: '/help', description: 'Show available commands', args: '' }
23+
];
24+
25+
/**
26+
* Built-in agents
27+
*/
28+
const BUILTIN_AGENTS = [
29+
{ name: '@code', description: 'Code generation agent' },
30+
{ name: '@test', description: 'Test generation agent' },
31+
{ name: '@doc', description: 'Documentation agent' },
32+
{ name: '@review', description: 'Code review agent' },
33+
{ name: '@refactor', description: 'Refactoring agent' },
34+
{ name: '@explain', description: 'Code explanation agent' }
35+
];
36+
37+
/**
38+
* Built-in variables
39+
*/
40+
const BUILTIN_VARIABLES = [
41+
{ name: '$selection', description: 'Current editor selection' },
42+
{ name: '$file', description: 'Current file path' },
43+
{ name: '$fileName', description: 'Current file name' },
44+
{ name: '$language', description: 'Current file language' },
45+
{ name: '$workspace', description: 'Workspace root path' },
46+
{ name: '$clipboard', description: 'Clipboard content' }
47+
];
48+
49+
/**
50+
* DevIns Completion Provider
51+
*/
52+
export class DevInsCompletionProvider implements vscode.CompletionItemProvider {
53+
private completionManager: CompletionManager | undefined;
54+
55+
constructor() {
56+
try {
57+
this.completionManager = new CompletionManager();
58+
} catch (e) {
59+
// mpp-core not available, use built-in completions only
60+
}
61+
}
62+
63+
async provideCompletionItems(
64+
document: vscode.TextDocument,
65+
position: vscode.Position,
66+
_token: vscode.CancellationToken,
67+
_context: vscode.CompletionContext
68+
): Promise<vscode.CompletionItem[]> {
69+
const linePrefix = document.lineAt(position).text.substring(0, position.character);
70+
const items: vscode.CompletionItem[] = [];
71+
72+
// Command completion (starts with /)
73+
if (linePrefix.endsWith('/') || /\/[a-zA-Z]*$/.test(linePrefix)) {
74+
items.push(...this.getCommandCompletions(linePrefix));
75+
}
76+
77+
// Agent completion (starts with @)
78+
if (linePrefix.endsWith('@') || /@[a-zA-Z]*$/.test(linePrefix)) {
79+
items.push(...this.getAgentCompletions(linePrefix));
80+
}
81+
82+
// Variable completion (starts with $)
83+
if (linePrefix.endsWith('$') || /\$[a-zA-Z]*$/.test(linePrefix)) {
84+
items.push(...this.getVariableCompletions(linePrefix));
85+
}
86+
87+
return items;
88+
}
89+
90+
private getCommandCompletions(linePrefix: string): vscode.CompletionItem[] {
91+
const prefix = linePrefix.match(/\/([a-zA-Z]*)$/)?.[1] || '';
92+
93+
return BUILTIN_COMMANDS
94+
.filter(cmd => cmd.name.substring(1).startsWith(prefix))
95+
.map(cmd => {
96+
const item = new vscode.CompletionItem(
97+
cmd.name,
98+
vscode.CompletionItemKind.Function
99+
);
100+
item.detail = cmd.description;
101+
item.insertText = cmd.name.substring(1) + cmd.args;
102+
item.documentation = new vscode.MarkdownString(`**${cmd.name}**\n\n${cmd.description}`);
103+
return item;
104+
});
105+
}
106+
107+
private getAgentCompletions(linePrefix: string): vscode.CompletionItem[] {
108+
const prefix = linePrefix.match(/@([a-zA-Z]*)$/)?.[1] || '';
109+
110+
return BUILTIN_AGENTS
111+
.filter(agent => agent.name.substring(1).startsWith(prefix))
112+
.map(agent => {
113+
const item = new vscode.CompletionItem(
114+
agent.name,
115+
vscode.CompletionItemKind.Class
116+
);
117+
item.detail = agent.description;
118+
item.insertText = agent.name.substring(1);
119+
item.documentation = new vscode.MarkdownString(`**${agent.name}**\n\n${agent.description}`);
120+
return item;
121+
});
122+
}
123+
124+
private getVariableCompletions(linePrefix: string): vscode.CompletionItem[] {
125+
const prefix = linePrefix.match(/\$([a-zA-Z]*)$/)?.[1] || '';
126+
127+
return BUILTIN_VARIABLES
128+
.filter(v => v.name.substring(1).startsWith(prefix))
129+
.map(v => {
130+
const item = new vscode.CompletionItem(
131+
v.name,
132+
vscode.CompletionItemKind.Variable
133+
);
134+
item.detail = v.description;
135+
item.insertText = v.name.substring(1);
136+
item.documentation = new vscode.MarkdownString(`**${v.name}**\n\n${v.description}`);
137+
return item;
138+
});
139+
}
140+
}
141+
142+
/**
143+
* Register DevIns completion provider
144+
*/
145+
export function registerDevInsCompletionProvider(
146+
context: vscode.ExtensionContext
147+
): vscode.Disposable {
148+
const provider = new DevInsCompletionProvider();
149+
150+
return vscode.languages.registerCompletionItemProvider(
151+
{ language: 'DevIns', scheme: 'file' },
152+
provider,
153+
'/', '@', '$'
154+
);
155+
}
156+
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* Status Bar Service - Shows AutoDev status in VSCode status bar
3+
*/
4+
5+
import * as vscode from 'vscode';
6+
7+
export type StatusBarState = 'idle' | 'thinking' | 'streaming' | 'error';
8+
9+
/**
10+
* Status Bar Manager for AutoDev
11+
*/
12+
export class StatusBarManager {
13+
private statusBarItem: vscode.StatusBarItem;
14+
private state: StatusBarState = 'idle';
15+
private animationInterval: NodeJS.Timeout | undefined;
16+
private animationFrame = 0;
17+
18+
constructor() {
19+
this.statusBarItem = vscode.window.createStatusBarItem(
20+
vscode.StatusBarAlignment.Right,
21+
100
22+
);
23+
this.statusBarItem.command = 'autodev.chat';
24+
this.statusBarItem.tooltip = 'Click to open AutoDev Chat';
25+
this.updateDisplay();
26+
this.statusBarItem.show();
27+
}
28+
29+
/**
30+
* Set the status bar state
31+
*/
32+
setState(state: StatusBarState, message?: string): void {
33+
this.state = state;
34+
this.stopAnimation();
35+
36+
if (state === 'thinking' || state === 'streaming') {
37+
this.startAnimation();
38+
}
39+
40+
this.updateDisplay(message);
41+
}
42+
43+
/**
44+
* Show a temporary message
45+
*/
46+
showMessage(message: string, timeout = 3000): void {
47+
const previousState = this.state;
48+
this.updateDisplay(message);
49+
50+
setTimeout(() => {
51+
if (this.state === previousState) {
52+
this.updateDisplay();
53+
}
54+
}, timeout);
55+
}
56+
57+
/**
58+
* Dispose the status bar item
59+
*/
60+
dispose(): void {
61+
this.stopAnimation();
62+
this.statusBarItem.dispose();
63+
}
64+
65+
private updateDisplay(message?: string): void {
66+
const icons: Record<StatusBarState, string> = {
67+
idle: '$(sparkle)',
68+
thinking: this.getThinkingIcon(),
69+
streaming: this.getStreamingIcon(),
70+
error: '$(error)'
71+
};
72+
73+
const colors: Record<StatusBarState, string | undefined> = {
74+
idle: undefined,
75+
thinking: new vscode.ThemeColor('statusBarItem.warningForeground').toString(),
76+
streaming: new vscode.ThemeColor('statusBarItem.prominentForeground').toString(),
77+
error: new vscode.ThemeColor('statusBarItem.errorForeground').toString()
78+
};
79+
80+
const icon = icons[this.state];
81+
const text = message || this.getDefaultText();
82+
83+
this.statusBarItem.text = `${icon} ${text}`;
84+
this.statusBarItem.backgroundColor = this.state === 'error'
85+
? new vscode.ThemeColor('statusBarItem.errorBackground')
86+
: undefined;
87+
}
88+
89+
private getDefaultText(): string {
90+
switch (this.state) {
91+
case 'idle':
92+
return 'AutoDev';
93+
case 'thinking':
94+
return 'Thinking...';
95+
case 'streaming':
96+
return 'Generating...';
97+
case 'error':
98+
return 'Error';
99+
}
100+
}
101+
102+
private getThinkingIcon(): string {
103+
const frames = ['$(loading~spin)', '$(sync~spin)', '$(gear~spin)'];
104+
return frames[this.animationFrame % frames.length];
105+
}
106+
107+
private getStreamingIcon(): string {
108+
const frames = ['$(pulse)', '$(radio-tower)', '$(broadcast)'];
109+
return frames[this.animationFrame % frames.length];
110+
}
111+
112+
private startAnimation(): void {
113+
this.animationFrame = 0;
114+
this.animationInterval = setInterval(() => {
115+
this.animationFrame++;
116+
this.updateDisplay();
117+
}, 500);
118+
}
119+
120+
private stopAnimation(): void {
121+
if (this.animationInterval) {
122+
clearInterval(this.animationInterval);
123+
this.animationInterval = undefined;
124+
}
125+
this.animationFrame = 0;
126+
}
127+
}
128+

0 commit comments

Comments
 (0)