Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support terminal intelligence #3390

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/core-browser/src/common/common.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,12 @@ export namespace TERMINAL_COMMANDS {
category: CATEGORY,
};

export const OPEN_TERMINAL_INTELL = {
id: 'terminal.intell',
label: '%terminal.intell%',
category: CATEGORY,
};

export const SEARCH_NEXT = {
id: 'terminal.search.next',
label: '%terminal.search.next%',
Expand Down
9 changes: 9 additions & 0 deletions packages/i18n/src/common/en-US.lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,14 @@ export const localizationBundle = {
'preference.terminal.integrated.copyOnSelection': 'Terminal > Copy On Selection',
'preference.terminal.integrated.copyOnSelectionDesc':
'Controls whether text selected in the terminal will be copied to the clipboard.',

'preference.terminal.integrated.enableTerminalIntellComplete': 'Enable Terminal Intell Complete',
'preference.terminal.integrated.terminalIntell': 'Terminal Intelligent Completion',
'preference.terminal.integrated.terminalIntellDesc':
'Automatically pop up subcommands, options, and context-related parameter completions during terminal input (Currently supports Bash only)',
'preference.terminal.integrated.terminalIntellUsage':
'Use Tab to trigger and select terminal suggestions, Esc to cancel, Enter to confirm',

// Local Echo
'preference.terminal.integrated.localEchoEnabled': 'Terminal > Local Echo',
'preference.terminal.integrated.localEchoDesc': 'When local echo should be enabled.',
Expand Down Expand Up @@ -1026,6 +1034,7 @@ export const localizationBundle = {
'terminal.or': 'Or',
'terminal.search': 'Search',
'terminal.search.next': 'Search Next',
'terminal.intell': 'Terminal IntelliSense',
'terminal.openWithPath': 'Open In Integrated Terminal',
'terminal.remove': 'Kill terminal',
'terminal.menu.search': 'Search',
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/common/zh-CN.lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,12 @@ export const localizationBundle = {
'preference.terminal.integrated.copyOnSelection': '终端选中复制',
'preference.terminal.integrated.copyOnSelectionDesc': '将终端中选中的文本立即复制到剪贴板。',

'preference.terminal.integrated.enableTerminalIntellComplete': '启用终端智能补全',
'preference.terminal.integrated.terminalIntell': '终端智能补全',
'preference.terminal.integrated.terminalIntellDesc':
'终端输入时,自动弹出弹出子命令、选项和上下文相关的参数的补全 (暂时仅支持 Bash)',
'preference.terminal.integrated.terminalIntellUsage': '使用 Tab 触发和选择终端提示,Esc 取消,Enter 键确认',

'preference.terminal.integrated.localEchoEnabled': '终端本地回显',
'preference.terminal.integrated.localEchoDesc': '何时应启用本地回显',
'preference.terminal.integrated.localEchoLatencyThreshold': '终端本地回显触发延时',
Expand Down Expand Up @@ -685,6 +691,7 @@ export const localizationBundle = {
'terminal.or': '或者',
'terminal.search': '搜索',
'terminal.search.next': '搜索下一个匹配项',
'terminal.intell': '终端智能',
'terminal.openWithPath': '在终端中打开',
'terminal.remove': '终止终端',
'terminal.relaunch': '重启终端',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,10 @@ export const defaultSettingSections: {
// 命令行参数
{ id: 'terminal.integrated.shellArgs.linux', localized: 'preference.terminal.integrated.shellArgs.linux' },
{ id: 'terminal.integrated.copyOnSelection', localized: 'preference.terminal.integrated.copyOnSelection' },
{
id: 'terminal.integrated.enableTerminalIntellComplete',
localized: 'preference.terminal.integrated.enableTerminalIntellComplete',
},
// Local echo
{ id: 'terminal.integrated.localEchoEnabled', localized: 'preference.terminal.integrated.localEchoEnabled' },
{
Expand Down
2 changes: 2 additions & 0 deletions packages/terminal-next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@opensumi/ide-core-common": "workspace:*",
"@opensumi/ide-core-node": "workspace:*",
"@opensumi/ide-file-service": "workspace:*",
"@withfig/autocomplete": "^2.657.0",
"node-pty": "1.0.0",
"os-locale": "^4.0.0",
"xterm": "5.3.0",
Expand All @@ -41,6 +42,7 @@
"@opensumi/ide-variable": "workspace:*",
"@opensumi/ide-workspace": "workspace:*",
"@types/http-proxy": "^1.17.2",
"@withfig/autocomplete-types": "^1.28.0",
"http-proxy": "^1.18.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.suggestions {
display: flex;
flex-direction: column;
background-color: var(--editorGroupHeader-tabsBackground);
color: var(--ai-native-text-color-common);
max-height: 350px;
width: 500px;
overflow-y: auto;
position: absolute;
bottom: 100%;
left: 0;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}

.suggestionItem {
padding: 6px 10px 6px 16px;
cursor: pointer;
}

.suggestionItemContainer {
display: flex;
flex-direction: column;
}

.suggestionDesc {
font-size: 12px;
}

.suggestionCmd {
font-size: 12px;
opacity: 0.6;
}

.suggestionItem:hover {
filter: brightness(110%);
background-color: var(--selection-background);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React, { useEffect, useState } from 'react';

import { Emitter } from '@opensumi/ide-core-common';

import styles from './terminal-intell-command-controller.module.less';

export interface SmartCommandDesc {
description: string;
command: string;
}

// 支持键盘选择的列表
export const KeyboardSelectableList = (props: {
items: { description: string; command: string }[];
handleSuggestionClick: (command: string) => void;
controller?: Emitter<string>;
noListen?: boolean;
}) => {
const { items, handleSuggestionClick, noListen = false, controller } = props;
// 选中项的索引,默认为最后一个
const [selectedIndex, setSelectedIndex] = useState(-1);

// 处理键盘事件
const handleKeyPress = (event: KeyboardEvent) => {
switch (event.key) {
case 'ArrowUp': // 上键
setSelectedIndex((prevIndex) => Math.max(prevIndex - 1, 0));
break;
case 'ArrowDown': // 下键
setSelectedIndex((prevIndex) =>
Math.min(prevIndex + 1, items.length - 1),
);
break;
case 'Enter': // 回车键
if (items[selectedIndex]) {
handleSuggestionClick(items[selectedIndex].command);
}
break;
default:
break;
}
};

// 添加全局键盘事件监听器
useEffect(() => {
if (noListen) {return;}
window.addEventListener('keydown', handleKeyPress);
return () => {
window.removeEventListener('keydown', handleKeyPress);
};
}, [items, selectedIndex]);

useEffect(() => {
if (!controller) {return;}
const disposable = controller.event((e: string) => {
if (e === 'ArrowUp') {
setSelectedIndex((prevIndex) => Math.max(prevIndex - 1, 0));
}
if (e === 'ArrowDown' || e === 'Tab') {
setSelectedIndex((prevIndex) =>
Math.min(prevIndex + 1, items.length - 1),
);
}
if (e === 'Enter') {
if (items[selectedIndex]) {
handleSuggestionClick(items[selectedIndex].command);
}
}
});

return () => {
disposable.dispose();
};
}, [controller, selectedIndex, items]);

useEffect(() => {
// HACK 定位到顶部
setSelectedIndex(0);
}, [items]);

return (
<div className={styles.suggestions}>
{items.map((cmd, index) => (
<div
key={index}
className={styles.suggestionItem}
style={{ backgroundColor: index === selectedIndex ? 'var(--selection-background)' : '' }}
onClick={() => handleSuggestionClick(cmd.command)}
>
<div className={styles.suggestionItemContainer}>
<div className={styles.suggestionDesc}>{cmd.description}</div>
<div className={styles.suggestionCmd}>{cmd.command}</div>
</div>
</div>
))}
</div>
);
};

export const TerminalIntellCommandController = (props: {
suggestions: SmartCommandDesc[];
controller: Emitter<string>;
onSuggestion: (suggestion: string) => void;
}) => {
const { suggestions, controller, onSuggestion } = props;

return (
<div style={{ position: 'relative' }}>
{suggestions.length > 0 && (
<KeyboardSelectableList
items={suggestions}
controller={controller}
handleSuggestionClick={onSuggestion}
noListen
/>
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
.suggestions {
display: flex;
background-color: transparent;
position: absolute;
bottom: 100%;
left: 0;

// TODO 考虑动态设置字体
font-family: Menlo, Monaco, 'Courier New', monospace;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

调整成 opensumi 全局用的 font-family 吧,代码库里搜一下

z-index: 12;
}

.suggestionList {
display: flex;
flex-direction: column;
max-height: 200px;
width: 450px;
overflow-y: scroll;
border-radius: 8px;
background-color: var(--vscode-editorSuggestWidget-background);
color: var(--vscode-editorSuggestWidget-foreground);
// TODO 适配白色主题
box-shadow: rgba(0, 0, 0, 0.133) 0px 3.2px 7.2px 0px, rgba(0, 0, 0, 0.11) 0px 0.6px 1.8px 0px;
}

.suggestionItem {
padding: 8px 10px 8px 8px;
cursor: pointer;
}

.suggestionItemContainer {
display: flex;
align-items: center;
}

.suggestionDesc {
font-size: 11px;
opacity: 0.6;
margin-left: 8px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

.suggestionIcon {
margin-right: 8px;
}

.suggestionCmd {
font-size: 12px;
white-space: nowrap;
flex: 1;
}

.suggestionItem:hover {
// filter: brightness(110%);
// background-color: var(--selection-background);
}

// 选中时旁边展示详情的容器
.suggestionItemExtraContainer {
background-color: #333;
color: #fff;
padding: 10px;
border-radius: 4px;
width: 300px;

position: fixed; /* 全局定位 */
top: 0;
transform: translateY(0); /* 修正浮动层位置 */

&.extraContainerLeft {
left: 100%; /* 在列表项的右侧 */
margin-left: 10px;
}

&.extraContainerRight {
right: 100%; /* 在列表项的左侧 */
margin-right: 10px;
}
}

.suggestionItemExtraCommand {
font-weight: bold;
margin-bottom: 8px;
font-size: 14px;
}

.suggestionItemExtraDescription {
opacity: 0.6;
}
Loading
Loading