Syntax highlighting fixes, autocomplete overhaul, tui changes
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
.PHONY: tui gentui tui-debug tui-test web install stop
|
.PHONY: tui gentui tui-debug tui-test web install stop
|
||||||
|
|
||||||
install:
|
install:
|
||||||
uv pip install fastapi uvicorn textual httpx tree-sitter-markdown tree-sitter-python tree-sitter-java tree-sitter-cpp tree-sitter-rust tree-sitter-go tree-sitter-javascript tree-sitter-typescript tree-sitter-c-sharp tree-sitter-html
|
uv pip install fastapi uvicorn textual httpx tree-sitter tree-sitter-markdown tree-sitter-python tree-sitter-java tree-sitter-cpp tree-sitter-rust tree-sitter-go tree-sitter-javascript tree-sitter-typescript tree-sitter-c-sharp tree-sitter-html tree-sitter-lua
|
||||||
devbox run -- bash -c "cd frontend && npm install"
|
devbox run -- bash -c "cd frontend && npm install"
|
||||||
|
|
||||||
stop:
|
stop:
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"llm": {
|
||||||
|
"base_url": "http://127.0.0.1:1010",
|
||||||
|
"model": "local-model",
|
||||||
|
"api_key": "not-needed",
|
||||||
|
"temperature": 0.9,
|
||||||
|
"max_tokens": 8120,
|
||||||
|
"timeout_seconds": 120.0
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 8000,
|
||||||
|
"public": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Editor } from '@monaco-editor/react';
|
import { Editor } from '@monaco-editor/react';
|
||||||
import { registerMonacoCompletions } from '../utils/monacoCompletions';
|
import { registerMonacoCompletions, getMonacoLanguage, getLanguageFileName } from '../utils/monacoCompletions';
|
||||||
import {
|
import {
|
||||||
getChallenges,
|
getChallenges,
|
||||||
runCode,
|
runCode,
|
||||||
@@ -184,7 +184,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
const inputToSend = overrideStdin !== undefined ? overrideStdin : stdinText;
|
const inputToSend = overrideStdin !== undefined ? overrideStdin : stdinText;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await runCode(currentChallenge.language.toLowerCase() || 'python', code, inputToSend);
|
const res = await runCode(getMonacoLanguage(currentChallenge.language), code, inputToSend);
|
||||||
if (res.exit_code === 0) {
|
if (res.exit_code === 0) {
|
||||||
setOutput({
|
setOutput({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
@@ -210,7 +210,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
setIsLinting(true);
|
setIsLinting(true);
|
||||||
setOutput({ status: 'idle', text: 'Running syntax & style lint...' });
|
setOutput({ status: 'idle', text: 'Running syntax & style lint...' });
|
||||||
try {
|
try {
|
||||||
const res = await lintCode(currentChallenge.language.toLowerCase() || 'python', code);
|
const res = await lintCode(getMonacoLanguage(currentChallenge.language), code);
|
||||||
if (res.exit_code === 0) {
|
if (res.exit_code === 0) {
|
||||||
setOutput({ status: 'success', text: '✓ Syntax & Style clean! No linting errors detected.' });
|
setOutput({ status: 'success', text: '✓ Syntax & Style clean! No linting errors detected.' });
|
||||||
} else {
|
} else {
|
||||||
@@ -237,7 +237,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
try {
|
try {
|
||||||
const res = await getGuidance(
|
const res = await getGuidance(
|
||||||
cid,
|
cid,
|
||||||
currentChallenge.language.toLowerCase() || 'python',
|
getMonacoLanguage(currentChallenge.language),
|
||||||
code,
|
code,
|
||||||
q
|
q
|
||||||
);
|
);
|
||||||
@@ -519,7 +519,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
{/* Middle Column: Monaco Code Editor & Resizable Output Console */}
|
{/* Middle Column: Monaco Code Editor & Resizable Output Console */}
|
||||||
<div className="flex-1 flex flex-col bg-[#1e1e1e] overflow-hidden min-w-0 min-h-0">
|
<div className="flex-1 flex flex-col bg-[#1e1e1e] overflow-hidden min-w-0 min-h-0">
|
||||||
<div className="bg-darkBg text-gray-400 px-4 py-2 text-xs flex justify-between items-center border-b border-darkBorder shrink-0">
|
<div className="bg-darkBg text-gray-400 px-4 py-2 text-xs flex justify-between items-center border-b border-darkBorder shrink-0">
|
||||||
<span className="font-mono text-blue-400">main.py</span>
|
<span className="font-mono text-blue-400">{getLanguageFileName(currentChallenge?.language)}</span>
|
||||||
<span className="text-[11px] text-gray-500">TactiTerm IDE</span>
|
<span className="text-[11px] text-gray-500">TactiTerm IDE</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -527,8 +527,8 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
|
|||||||
<div className="flex-1 min-h-0 relative">
|
<div className="flex-1 min-h-0 relative">
|
||||||
<Editor
|
<Editor
|
||||||
height="100%"
|
height="100%"
|
||||||
defaultLanguage={currentChallenge?.language.toLowerCase() || 'python'}
|
defaultLanguage={getMonacoLanguage(currentChallenge?.language)}
|
||||||
language={currentChallenge?.language.toLowerCase() || 'python'}
|
language={getMonacoLanguage(currentChallenge?.language)}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
value={code}
|
value={code}
|
||||||
theme="vs-dark"
|
theme="vs-dark"
|
||||||
|
|||||||
@@ -1,12 +1,191 @@
|
|||||||
import type { Monaco } from '@monaco-editor/react';
|
import type { Monaco } from '@monaco-editor/react';
|
||||||
|
|
||||||
|
export function getMonacoLanguage(language?: string): string {
|
||||||
|
if (!language) return 'python';
|
||||||
|
const norm = language.toLowerCase().trim();
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
'c++': 'cpp',
|
||||||
|
'cpp': 'cpp',
|
||||||
|
'cxx': 'cpp',
|
||||||
|
'c#': 'csharp',
|
||||||
|
'cs': 'csharp',
|
||||||
|
'csharp': 'csharp',
|
||||||
|
'js': 'javascript',
|
||||||
|
'javascript': 'javascript',
|
||||||
|
'ts': 'typescript',
|
||||||
|
'typescript': 'typescript',
|
||||||
|
'py': 'python',
|
||||||
|
'python': 'python',
|
||||||
|
'rs': 'rust',
|
||||||
|
'rust': 'rust',
|
||||||
|
'golang': 'go',
|
||||||
|
'go': 'go',
|
||||||
|
'html': 'html',
|
||||||
|
'java': 'java',
|
||||||
|
'lua': 'lua',
|
||||||
|
};
|
||||||
|
return map[norm] || norm;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLanguageFileName(language?: string): string {
|
||||||
|
const lang = getMonacoLanguage(language);
|
||||||
|
const extMap: Record<string, string> = {
|
||||||
|
python: 'main.py',
|
||||||
|
csharp: 'Program.cs',
|
||||||
|
cpp: 'main.cpp',
|
||||||
|
java: 'Main.java',
|
||||||
|
javascript: 'main.js',
|
||||||
|
typescript: 'main.ts',
|
||||||
|
rust: 'main.rs',
|
||||||
|
lua: 'main.lua',
|
||||||
|
html: 'index.html',
|
||||||
|
go: 'main.go',
|
||||||
|
};
|
||||||
|
return extMap[lang] || 'main.txt';
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractDocumentSymbols(code: string): string[] {
|
||||||
|
const words = code.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || [];
|
||||||
|
const unique = new Set<string>();
|
||||||
|
const reserved = new Set([
|
||||||
|
'if', 'else', 'for', 'while', 'return', 'import', 'from', 'def', 'class',
|
||||||
|
'public', 'private', 'protected', 'static', 'void', 'int', 'double', 'float',
|
||||||
|
'bool', 'boolean', 'char', 'const', 'let', 'var', 'func', 'package', 'struct',
|
||||||
|
]);
|
||||||
|
for (const w of words) {
|
||||||
|
if (w.length > 2 && !reserved.has(w)) {
|
||||||
|
unique.add(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(unique);
|
||||||
|
}
|
||||||
|
|
||||||
let registered = false;
|
let registered = false;
|
||||||
|
|
||||||
export function registerMonacoCompletions(monaco: Monaco) {
|
export function registerMonacoCompletions(monaco: Monaco) {
|
||||||
if (registered) return;
|
if (registered) return;
|
||||||
registered = true;
|
registered = true;
|
||||||
|
|
||||||
// Completion items for Python
|
// ── JAVA COMPLETER ──────────────────────────────────────────
|
||||||
|
monaco.languages.registerCompletionItemProvider('java', {
|
||||||
|
triggerCharacters: ['.'],
|
||||||
|
provideCompletionItems: (model, position) => {
|
||||||
|
const lineUntilPosition = model.getValueInRange({
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
endColumn: position.column,
|
||||||
|
});
|
||||||
|
|
||||||
|
const word = model.getWordUntilPosition(position);
|
||||||
|
const range = {
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
startColumn: word.startColumn,
|
||||||
|
endColumn: word.endColumn,
|
||||||
|
};
|
||||||
|
|
||||||
|
const code = model.getValue();
|
||||||
|
const dotMatch = lineUntilPosition.match(/([a-zA-Z_][a-zA-Z0-9_]*)\.\s*$/);
|
||||||
|
|
||||||
|
if (dotMatch) {
|
||||||
|
const objName = dotMatch[1];
|
||||||
|
const lowerName = objName.toLowerCase();
|
||||||
|
let targetType = 'unknown';
|
||||||
|
|
||||||
|
if (new RegExp(`Map<|HashMap<|TreeMap<`, 'i').test(code) && (lowerName.includes('map') || lowerName.includes('dict'))) {
|
||||||
|
targetType = 'map';
|
||||||
|
} else if (new RegExp(`List<|ArrayList<|LinkedList<`, 'i').test(code) && (lowerName.includes('list') || lowerName.includes('arr') || lowerName.includes('items'))) {
|
||||||
|
targetType = 'list';
|
||||||
|
} else if (new RegExp(`Set<|HashSet<`, 'i').test(code) && lowerName.includes('set')) {
|
||||||
|
targetType = 'set';
|
||||||
|
} else if (lowerName === 'out' || objName === 'System') {
|
||||||
|
targetType = 'system';
|
||||||
|
} else if (lowerName.includes('str') || lowerName.includes('text') || lowerName.includes('name') || lowerName.includes('msg')) {
|
||||||
|
targetType = 'string';
|
||||||
|
} else {
|
||||||
|
if (lowerName.includes('map') || lowerName.includes('dict')) targetType = 'map';
|
||||||
|
else if (lowerName.includes('list') || lowerName.includes('arr')) targetType = 'list';
|
||||||
|
else if (lowerName.includes('set')) targetType = 'set';
|
||||||
|
else targetType = 'general';
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapMethods = [
|
||||||
|
{ label: 'put', insertText: 'put(${1:key}, ${2:value})', doc: 'Associates specified value with key in map.' },
|
||||||
|
{ label: 'get', insertText: 'get(${1:key})', doc: 'Returns value mapped to specified key.' },
|
||||||
|
{ label: 'containsKey', insertText: 'containsKey(${1:key})', doc: 'Returns true if map contains key.' },
|
||||||
|
{ label: 'containsValue', insertText: 'containsValue(${1:value})', doc: 'Returns true if map contains value.' },
|
||||||
|
{ label: 'size', insertText: 'size()', doc: 'Returns number of key-value mappings.' },
|
||||||
|
{ label: 'isEmpty', insertText: 'isEmpty()', doc: 'Returns true if map contains no mappings.' },
|
||||||
|
{ label: 'keySet', insertText: 'keySet()', doc: 'Returns Set view of keys in map.' },
|
||||||
|
{ label: 'values', insertText: 'values()', doc: 'Returns Collection view of values in map.' },
|
||||||
|
{ label: 'entrySet', insertText: 'entrySet()', doc: 'Returns Set view of mappings in map.' },
|
||||||
|
{ label: 'remove', insertText: 'remove(${1:key})', doc: 'Removes mapping for key.' },
|
||||||
|
{ label: 'clear', insertText: 'clear()', doc: 'Removes all mappings from map.' },
|
||||||
|
{ label: 'getOrDefault', insertText: 'getOrDefault(${1:key}, ${2:defaultValue})', doc: 'Returns mapped value or default.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const listMethods = [
|
||||||
|
{ label: 'add', insertText: 'add(${1:element})', doc: 'Appends element to end of list.' },
|
||||||
|
{ label: 'get', insertText: 'get(${1:index})', doc: 'Returns element at index.' },
|
||||||
|
{ label: 'size', insertText: 'size()', doc: 'Returns number of elements.' },
|
||||||
|
{ label: 'remove', insertText: 'remove(${1:index})', doc: 'Removes element at index.' },
|
||||||
|
{ label: 'contains', insertText: 'contains(${1:element})', doc: 'Returns true if list contains element.' },
|
||||||
|
{ label: 'indexOf', insertText: 'indexOf(${1:element})', doc: 'Returns index of element.' },
|
||||||
|
{ label: 'isEmpty', insertText: 'isEmpty()', doc: 'Returns true if list contains no elements.' },
|
||||||
|
{ label: 'clear', insertText: 'clear()', doc: 'Removes all elements.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const stringMethods = [
|
||||||
|
{ label: 'length', insertText: 'length()', doc: 'Returns length of string.' },
|
||||||
|
{ label: 'substring', insertText: 'substring(${1:beginIndex})', doc: 'Returns substring starting at beginIndex.' },
|
||||||
|
{ label: 'charAt', insertText: 'charAt(${1:index})', doc: 'Returns char value at index.' },
|
||||||
|
{ label: 'toLowerCase', insertText: 'toLowerCase()', doc: 'Converts to lowercase.' },
|
||||||
|
{ label: 'toUpperCase', insertText: 'toUpperCase()', doc: 'Converts to uppercase.' },
|
||||||
|
{ label: 'trim', insertText: 'trim()', doc: 'Removes leading/trailing whitespace.' },
|
||||||
|
{ label: 'split', insertText: 'split("${1:regex}")', doc: 'Splits string around matches.' },
|
||||||
|
{ label: 'contains', insertText: 'contains("${1:str}")', doc: 'Returns true if contains string.' },
|
||||||
|
{ label: 'startsWith', insertText: 'startsWith("${1:prefix}")', doc: 'Checks if starts with prefix.' },
|
||||||
|
{ label: 'equals', insertText: 'equals(${1:anObject})', doc: 'Compares to specified object.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const systemMethods = [
|
||||||
|
{ label: 'println', insertText: 'println(${1:value});', doc: 'Prints value and terminates line.' },
|
||||||
|
{ label: 'printf', insertText: 'printf("${1:%s}\\n", ${2:args});', doc: 'Prints formatted string.' },
|
||||||
|
{ label: 'print', insertText: 'print(${1:value});', doc: 'Prints value.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
let selected = mapMethods;
|
||||||
|
if (targetType === 'list') selected = listMethods;
|
||||||
|
else if (targetType === 'string') selected = stringMethods;
|
||||||
|
else if (targetType === 'system') selected = systemMethods;
|
||||||
|
else if (targetType === 'general') selected = [...mapMethods, ...listMethods, ...stringMethods];
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: selected.map(m => ({
|
||||||
|
label: m.label,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: m.insertText,
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: m.doc,
|
||||||
|
range,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbols = extractDocumentSymbols(code);
|
||||||
|
const suggestions = [
|
||||||
|
{ label: 'System.out.println', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'System.out.println(${1:value});', range },
|
||||||
|
{ label: 'HashMap', kind: monaco.languages.CompletionItemKind.Class, insertText: 'Map<${1:String}, ${2:Integer}> ${3:map} = new HashMap<>();', range },
|
||||||
|
{ label: 'ArrayList', kind: monaco.languages.CompletionItemKind.Class, insertText: 'List<${1:String}> ${2:list} = new ArrayList<>();', range },
|
||||||
|
...symbols.map(s => ({ label: s, kind: monaco.languages.CompletionItemKind.Variable, insertText: s, range })),
|
||||||
|
];
|
||||||
|
|
||||||
|
return { suggestions };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── PYTHON COMPLETER ────────────────────────────────────────
|
||||||
monaco.languages.registerCompletionItemProvider('python', {
|
monaco.languages.registerCompletionItemProvider('python', {
|
||||||
triggerCharacters: ['.'],
|
triggerCharacters: ['.'],
|
||||||
provideCompletionItems: (model, position) => {
|
provideCompletionItems: (model, position) => {
|
||||||
@@ -25,70 +204,194 @@ export function registerMonacoCompletions(monaco: Monaco) {
|
|||||||
endColumn: word.endColumn,
|
endColumn: word.endColumn,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const code = model.getValue();
|
||||||
|
const dotMatch = lineUntilPosition.match(/([a-zA-Z_][a-zA-Z0-9_]*)\.\s*$/);
|
||||||
|
|
||||||
|
if (dotMatch) {
|
||||||
|
const objName = dotMatch[1].toLowerCase();
|
||||||
|
let methods = [
|
||||||
|
{ label: 'get', insertText: 'get(${1:key})', doc: 'Returns value for key.' },
|
||||||
|
{ label: 'keys', insertText: 'keys()', doc: 'Returns dictionary keys.' },
|
||||||
|
{ label: 'values', insertText: 'values()', doc: 'Returns dictionary values.' },
|
||||||
|
{ label: 'items', insertText: 'items()', doc: 'Returns key-value pairs.' },
|
||||||
|
{ label: 'append', insertText: 'append(${1:item})', doc: 'Appends item to list.' },
|
||||||
|
{ label: 'pop', insertText: 'pop(${1:index})', doc: 'Removes and returns item.' },
|
||||||
|
{ label: 'split', insertText: 'split("${1:sep}")', doc: 'Splits string.' },
|
||||||
|
{ label: 'join', insertText: 'join(${1:iterable})', doc: 'Joins elements.' },
|
||||||
|
];
|
||||||
|
if (objName.includes('map') || objName.includes('dict')) {
|
||||||
|
methods = [
|
||||||
|
{ label: 'get', insertText: 'get(${1:key})', doc: 'Returns value for key.' },
|
||||||
|
{ label: 'keys', insertText: 'keys()', doc: 'Returns dictionary keys.' },
|
||||||
|
{ label: 'values', insertText: 'values()', doc: 'Returns dictionary values.' },
|
||||||
|
{ label: 'items', insertText: 'items()', doc: 'Returns key-value pairs.' },
|
||||||
|
{ label: 'update', insertText: 'update(${1:dict})', doc: 'Updates dictionary.' },
|
||||||
|
{ label: 'pop', insertText: 'pop(${1:key})', doc: 'Removes key.' },
|
||||||
|
];
|
||||||
|
} else if (objName.includes('list') || objName.includes('arr')) {
|
||||||
|
methods = [
|
||||||
|
{ label: 'append', insertText: 'append(${1:item})', doc: 'Appends item.' },
|
||||||
|
{ label: 'extend', insertText: 'extend(${1:iterable})', doc: 'Extends list.' },
|
||||||
|
{ label: 'insert', insertText: 'insert(${1:index}, ${2:item})', doc: 'Inserts item.' },
|
||||||
|
{ label: 'remove', insertText: 'remove(${1:item})', doc: 'Removes item.' },
|
||||||
|
{ label: 'sort', insertText: 'sort()', doc: 'Sorts list.' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: methods.map(m => ({
|
||||||
|
label: m.label,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: m.insertText,
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: m.doc,
|
||||||
|
range,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbols = extractDocumentSymbols(code);
|
||||||
const suggestions = [
|
const suggestions = [
|
||||||
{
|
{ label: 'print', kind: monaco.languages.CompletionItemKind.Function, insertText: 'print(${1:value})', range },
|
||||||
label: 'print',
|
{ label: 'len', kind: monaco.languages.CompletionItemKind.Function, insertText: 'len(${1:obj})', range },
|
||||||
kind: monaco.languages.CompletionItemKind.Function,
|
{ label: 'range', kind: monaco.languages.CompletionItemKind.Function, insertText: 'range(${1:stop})', range },
|
||||||
insertText: 'print(${1:value})',
|
{ label: 'enumerate', kind: monaco.languages.CompletionItemKind.Function, insertText: 'enumerate(${1:iterable})', range },
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
...symbols.map(s => ({ label: s, kind: monaco.languages.CompletionItemKind.Variable, insertText: s, range })),
|
||||||
documentation: 'Prints specified objects to standard output.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'len',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Function,
|
|
||||||
insertText: 'len(${1:obj})',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Returns the number of items in a container.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'range',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Function,
|
|
||||||
insertText: 'range(${1:stop})',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Generates a sequence of numbers.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'enumerate',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Function,
|
|
||||||
insertText: 'enumerate(${1:iterable})',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Yields index and value tuples from an iterable.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'append',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Method,
|
|
||||||
insertText: 'append(${1:item})',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Appends a new item to the end of the list.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'split',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Method,
|
|
||||||
insertText: 'split(${1:sep})',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Splits string into a list of substrings.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'def',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
|
||||||
insertText: 'def ${1:function_name}(${2:params}):\n ${3:pass}',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Defines a Python function.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return { suggestions };
|
return { suggestions };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Completion items for JavaScript / TypeScript
|
// ── C++ COMPLETER ───────────────────────────────────────────
|
||||||
|
monaco.languages.registerCompletionItemProvider('cpp', {
|
||||||
|
triggerCharacters: ['.', ':', '>'],
|
||||||
|
provideCompletionItems: (model, position) => {
|
||||||
|
const lineUntilPosition = model.getValueInRange({
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
endColumn: position.column,
|
||||||
|
});
|
||||||
|
|
||||||
|
const word = model.getWordUntilPosition(position);
|
||||||
|
const range = {
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
startColumn: word.startColumn,
|
||||||
|
endColumn: word.endColumn,
|
||||||
|
};
|
||||||
|
|
||||||
|
const code = model.getValue();
|
||||||
|
const dotMatch = lineUntilPosition.match(/([a-zA-Z_][a-zA-Z0-9_]*|\bstd\b)(\.|\:\:|->)\s*$/);
|
||||||
|
|
||||||
|
if (dotMatch) {
|
||||||
|
const objName = dotMatch[1].toLowerCase();
|
||||||
|
let methods = [
|
||||||
|
{ label: 'push_back', insertText: 'push_back(${1:val})', doc: 'Appends element.' },
|
||||||
|
{ label: 'pop_back', insertText: 'pop_back()', doc: 'Removes last element.' },
|
||||||
|
{ label: 'size', insertText: 'size()', doc: 'Returns number of elements.' },
|
||||||
|
{ label: 'empty', insertText: 'empty()', doc: 'Checks if container is empty.' },
|
||||||
|
{ label: 'find', insertText: 'find(${1:key})', doc: 'Finds element.' },
|
||||||
|
{ label: 'insert', insertText: 'insert(${1:val})', doc: 'Inserts element.' },
|
||||||
|
{ label: 'clear', insertText: 'clear()', doc: 'Clears all elements.' },
|
||||||
|
];
|
||||||
|
if (objName === 'std') {
|
||||||
|
methods = [
|
||||||
|
{ label: 'cout', insertText: 'cout << ${1:value} << std::endl;', doc: 'Standard output stream.' },
|
||||||
|
{ label: 'cin', insertText: 'cin >> ${1:var};', doc: 'Standard input stream.' },
|
||||||
|
{ label: 'vector', insertText: 'vector<${1:int}> ${2:vec};', doc: 'Dynamic array container.' },
|
||||||
|
{ label: 'map', insertText: 'map<${1:string}, ${2:int}> ${3:map};', doc: 'Sorted associative container.' },
|
||||||
|
{ label: 'sort', insertText: 'sort(${1:begin}, ${2:end});', doc: 'Sorts elements.' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: methods.map(m => ({
|
||||||
|
label: m.label,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: m.insertText,
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: m.doc,
|
||||||
|
range,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbols = extractDocumentSymbols(code);
|
||||||
|
return {
|
||||||
|
suggestions: [
|
||||||
|
{ label: 'std::cout', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'std::cout << ${1:value} << std::endl;', range },
|
||||||
|
{ label: 'std::vector', kind: monaco.languages.CompletionItemKind.Class, insertText: 'std::vector<${1:int}> ${2:vec};', range },
|
||||||
|
...symbols.map(s => ({ label: s, kind: monaco.languages.CompletionItemKind.Variable, insertText: s, range })),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── C# COMPLETER ────────────────────────────────────────────
|
||||||
|
monaco.languages.registerCompletionItemProvider('csharp', {
|
||||||
|
triggerCharacters: ['.'],
|
||||||
|
provideCompletionItems: (model, position) => {
|
||||||
|
const lineUntilPosition = model.getValueInRange({
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
endColumn: position.column,
|
||||||
|
});
|
||||||
|
|
||||||
|
const word = model.getWordUntilPosition(position);
|
||||||
|
const range = {
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
startColumn: word.startColumn,
|
||||||
|
endColumn: word.endColumn,
|
||||||
|
};
|
||||||
|
|
||||||
|
const code = model.getValue();
|
||||||
|
const dotMatch = lineUntilPosition.match(/([a-zA-Z_][a-zA-Z0-9_]*)\.\s*$/);
|
||||||
|
|
||||||
|
if (dotMatch) {
|
||||||
|
const objName = dotMatch[1].toLowerCase();
|
||||||
|
let methods = [
|
||||||
|
{ label: 'Add', insertText: 'Add(${1:val});', doc: 'Adds value.' },
|
||||||
|
{ label: 'Remove', insertText: 'Remove(${1:val});', doc: 'Removes value.' },
|
||||||
|
{ label: 'ContainsKey', insertText: 'ContainsKey(${1:key})', doc: 'Checks if dictionary contains key.' },
|
||||||
|
{ label: 'Count', insertText: 'Count', doc: 'Gets number of elements.' },
|
||||||
|
{ label: 'Clear', insertText: 'Clear()', doc: 'Removes all elements.' },
|
||||||
|
];
|
||||||
|
if (objName === 'console') {
|
||||||
|
methods = [
|
||||||
|
{ label: 'WriteLine', insertText: 'WriteLine(${1:value});', doc: 'Writes value to standard output.' },
|
||||||
|
{ label: 'ReadLine', insertText: 'ReadLine()', doc: 'Reads next line.' },
|
||||||
|
{ label: 'Write', insertText: 'Write(${1:value});', doc: 'Writes value.' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: methods.map(m => ({
|
||||||
|
label: m.label,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: m.insertText,
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: m.doc,
|
||||||
|
range,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbols = extractDocumentSymbols(code);
|
||||||
|
return {
|
||||||
|
suggestions: [
|
||||||
|
{ label: 'Console.WriteLine', kind: monaco.languages.CompletionItemKind.Method, insertText: 'Console.WriteLine(${1:value});', range },
|
||||||
|
{ label: 'Dictionary', kind: monaco.languages.CompletionItemKind.Class, insertText: 'Dictionary<${1:string}, ${2:int}> ${3:dict} = new Dictionary<${1:string}, ${2:int}>();', range },
|
||||||
|
...symbols.map(s => ({ label: s, kind: monaco.languages.CompletionItemKind.Variable, insertText: s, range })),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── JS & TS COMPLETER ───────────────────────────────────────
|
||||||
['javascript', 'typescript'].forEach((lang) => {
|
['javascript', 'typescript'].forEach((lang) => {
|
||||||
monaco.languages.registerCompletionItemProvider(lang, {
|
monaco.languages.registerCompletionItemProvider(lang, {
|
||||||
triggerCharacters: ['.'],
|
triggerCharacters: ['.'],
|
||||||
@@ -108,73 +411,62 @@ export function registerMonacoCompletions(monaco: Monaco) {
|
|||||||
endColumn: word.endColumn,
|
endColumn: word.endColumn,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (lineUntilPosition.endsWith('console.')) {
|
const code = model.getValue();
|
||||||
|
const dotMatch = lineUntilPosition.match(/([a-zA-Z_][a-zA-Z0-9_]*)\.\s*$/);
|
||||||
|
|
||||||
|
if (dotMatch) {
|
||||||
|
const objName = dotMatch[1].toLowerCase();
|
||||||
|
let methods = [
|
||||||
|
{ label: 'map', insertText: 'map((${1:x}) => ${2:x})', doc: 'Creates a new array with mapped elements.' },
|
||||||
|
{ label: 'filter', insertText: 'filter((${1:x}) => ${2:true})', doc: 'Filters array.' },
|
||||||
|
{ label: 'push', insertText: 'push(${1:item})', doc: 'Appends element.' },
|
||||||
|
{ label: 'slice', insertText: 'slice(${1:start}, ${2:end})', doc: 'Returns section of array.' },
|
||||||
|
{ label: 'length', insertText: 'length', doc: 'Gets length.' },
|
||||||
|
{ label: 'get', insertText: 'get(${1:key})', doc: 'Gets Map element.' },
|
||||||
|
{ label: 'set', insertText: 'set(${1:key}, ${2:val})', doc: 'Sets Map element.' },
|
||||||
|
];
|
||||||
|
if (objName === 'console') {
|
||||||
|
methods = [
|
||||||
|
{ label: 'log', insertText: 'log(${1:msg});', doc: 'Outputs message.' },
|
||||||
|
{ label: 'error', insertText: 'error(${1:err});', doc: 'Outputs error.' },
|
||||||
|
{ label: 'warn', insertText: 'warn(${1:msg});', doc: 'Outputs warning.' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
suggestions: [
|
suggestions: methods.map(m => ({
|
||||||
{
|
label: m.label,
|
||||||
label: 'log',
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
kind: monaco.languages.CompletionItemKind.Method,
|
insertText: m.insertText,
|
||||||
insertText: 'log(${1:message});',
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
documentation: m.doc,
|
||||||
documentation: 'Outputs a message to the debugging console.',
|
range,
|
||||||
range,
|
})),
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'error',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Method,
|
|
||||||
insertText: 'error(${1:err});',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Outputs an error message to the console.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'warn',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Method,
|
|
||||||
insertText: 'warn(${1:msg});',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Outputs a warning message to the console.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const suggestions = [
|
const symbols = extractDocumentSymbols(code);
|
||||||
{
|
return {
|
||||||
label: 'console.log',
|
suggestions: [
|
||||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
{ label: 'console.log', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'console.log(${1:val});', range },
|
||||||
insertText: 'console.log(${1:val});',
|
...symbols.map(s => ({ label: s, kind: monaco.languages.CompletionItemKind.Variable, insertText: s, range })),
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
],
|
||||||
documentation: 'Print debug message to console.',
|
};
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'map',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Method,
|
|
||||||
insertText: 'map((${1:item}) => ${2:item})',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Array map transformation function.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'filter',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Method,
|
|
||||||
insertText: 'filter((${1:item}) => ${2:true})',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Array filter function.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return { suggestions };
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Completion items for Rust
|
// ── RUST COMPLETER ──────────────────────────────────────────
|
||||||
monaco.languages.registerCompletionItemProvider('rust', {
|
monaco.languages.registerCompletionItemProvider('rust', {
|
||||||
triggerCharacters: ['.', ':'],
|
triggerCharacters: ['.', ':'],
|
||||||
provideCompletionItems: (model, position) => {
|
provideCompletionItems: (model, position) => {
|
||||||
|
const lineUntilPosition = model.getValueInRange({
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
endColumn: position.column,
|
||||||
|
});
|
||||||
|
|
||||||
const word = model.getWordUntilPosition(position);
|
const word = model.getWordUntilPosition(position);
|
||||||
const range = {
|
const range = {
|
||||||
startLineNumber: position.lineNumber,
|
startLineNumber: position.lineNumber,
|
||||||
@@ -183,38 +475,43 @@ export function registerMonacoCompletions(monaco: Monaco) {
|
|||||||
endColumn: word.endColumn,
|
endColumn: word.endColumn,
|
||||||
};
|
};
|
||||||
|
|
||||||
const suggestions = [
|
const code = model.getValue();
|
||||||
{
|
const dotMatch = lineUntilPosition.match(/([a-zA-Z_][a-zA-Z0-9_]*|\bVec|\bHashMap)(\.|\:\:)\s*$/);
|
||||||
label: 'println!',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Function,
|
|
||||||
insertText: 'println!("${1:{}}", ${2:val});',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Prints formatted text to stdout.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'format!',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Function,
|
|
||||||
insertText: 'format!("${1:{}}", ${2:val})',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Constructs a formatted String.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Vec::push',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Method,
|
|
||||||
insertText: 'push(${1:val});',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Appends element to back of vector.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return { suggestions };
|
if (dotMatch) {
|
||||||
|
let methods = [
|
||||||
|
{ label: 'push', insertText: 'push(${1:val});', doc: 'Appends element.' },
|
||||||
|
{ label: 'insert', insertText: 'insert(${1:key}, ${2:val});', doc: 'Inserts key-value pair.' },
|
||||||
|
{ label: 'get', insertText: 'get(&${1:key})', doc: 'Returns reference to value.' },
|
||||||
|
{ label: 'len', insertText: 'len()', doc: 'Returns length.' },
|
||||||
|
{ label: 'is_empty', insertText: 'is_empty()', doc: 'Checks if empty.' },
|
||||||
|
{ label: 'iter', insertText: 'iter()', doc: 'Returns iterator.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: methods.map(m => ({
|
||||||
|
label: m.label,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: m.insertText,
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: m.doc,
|
||||||
|
range,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbols = extractDocumentSymbols(code);
|
||||||
|
return {
|
||||||
|
suggestions: [
|
||||||
|
{ label: 'println!', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'println!("${1:{}}", ${2:val});', range },
|
||||||
|
{ label: 'Vec::new', kind: monaco.languages.CompletionItemKind.Function, insertText: 'Vec::new()', range },
|
||||||
|
...symbols.map(s => ({ label: s, kind: monaco.languages.CompletionItemKind.Variable, insertText: s, range })),
|
||||||
|
],
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Completion items for Go
|
// ── GO COMPLETER ────────────────────────────────────────────
|
||||||
monaco.languages.registerCompletionItemProvider('go', {
|
monaco.languages.registerCompletionItemProvider('go', {
|
||||||
triggerCharacters: ['.'],
|
triggerCharacters: ['.'],
|
||||||
provideCompletionItems: (model, position) => {
|
provideCompletionItems: (model, position) => {
|
||||||
@@ -233,49 +530,113 @@ export function registerMonacoCompletions(monaco: Monaco) {
|
|||||||
endColumn: word.endColumn,
|
endColumn: word.endColumn,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (lineUntilPosition.endsWith('fmt.')) {
|
const code = model.getValue();
|
||||||
|
const dotMatch = lineUntilPosition.match(/([a-zA-Z_][a-zA-Z0-9_]*)\.\s*$/);
|
||||||
|
|
||||||
|
if (dotMatch) {
|
||||||
|
let methods = [
|
||||||
|
{ label: 'Println', insertText: 'Println(${1:v})', doc: 'Writes formatted line.' },
|
||||||
|
{ label: 'Printf', insertText: 'Printf("${1:%v}\\n", ${2:v})', doc: 'Writes formatted string.' },
|
||||||
|
{ label: 'Split', insertText: 'Split(${1:s}, "${2:sep}")', doc: 'Splits string.' },
|
||||||
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
suggestions: [
|
suggestions: methods.map(m => ({
|
||||||
{
|
label: m.label,
|
||||||
label: 'Println',
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
kind: monaco.languages.CompletionItemKind.Function,
|
insertText: m.insertText,
|
||||||
insertText: 'Println(${1:v})',
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
documentation: m.doc,
|
||||||
documentation: 'Formats using default formats and writes to standard output.',
|
range,
|
||||||
range,
|
})),
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Printf',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Function,
|
|
||||||
insertText: 'Printf("${1:%v}\\n", ${2:v})',
|
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
||||||
documentation: 'Formats according to specifier and writes to standard output.',
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const symbols = extractDocumentSymbols(code);
|
||||||
return {
|
return {
|
||||||
suggestions: [
|
suggestions: [
|
||||||
{
|
{ label: 'fmt.Println', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'fmt.Println(${1:v})', range },
|
||||||
label: 'fmt.Println',
|
{ label: 'make', kind: monaco.languages.CompletionItemKind.Function, insertText: 'make(${1:type}, ${2:len})', range },
|
||||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
...symbols.map(s => ({ label: s, kind: monaco.languages.CompletionItemKind.Variable, insertText: s, range })),
|
||||||
insertText: 'fmt.Println(${1:v})',
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── LUA COMPLETER ───────────────────────────────────────────
|
||||||
|
monaco.languages.registerCompletionItemProvider('lua', {
|
||||||
|
triggerCharacters: ['.'],
|
||||||
|
provideCompletionItems: (model, position) => {
|
||||||
|
const lineUntilPosition = model.getValueInRange({
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
endColumn: position.column,
|
||||||
|
});
|
||||||
|
|
||||||
|
const word = model.getWordUntilPosition(position);
|
||||||
|
const range = {
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
startColumn: word.startColumn,
|
||||||
|
endColumn: word.endColumn,
|
||||||
|
};
|
||||||
|
|
||||||
|
const code = model.getValue();
|
||||||
|
const dotMatch = lineUntilPosition.match(/([a-zA-Z_][a-zA-Z0-9_]*)\.\s*$/);
|
||||||
|
|
||||||
|
if (dotMatch) {
|
||||||
|
let methods = [
|
||||||
|
{ label: 'insert', insertText: 'insert(${1:t}, ${2:val})', doc: 'Inserts element.' },
|
||||||
|
{ label: 'remove', insertText: 'remove(${1:t}, ${2:pos})', doc: 'Removes element.' },
|
||||||
|
{ label: 'sub', insertText: 'sub(${1:s}, ${2:i}, ${3:j})', doc: 'Returns substring.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: methods.map(m => ({
|
||||||
|
label: m.label,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: m.insertText,
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
documentation: 'Print line to stdout.',
|
documentation: m.doc,
|
||||||
range,
|
range,
|
||||||
},
|
})),
|
||||||
{
|
};
|
||||||
label: 'make',
|
}
|
||||||
kind: monaco.languages.CompletionItemKind.Function,
|
|
||||||
insertText: 'make(${1:type}, ${2:len})',
|
const symbols = extractDocumentSymbols(code);
|
||||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
return {
|
||||||
documentation: 'Allocates slice, map, or channel.',
|
suggestions: [
|
||||||
range,
|
{ label: 'print', kind: monaco.languages.CompletionItemKind.Function, insertText: 'print(${1:value})', range },
|
||||||
},
|
...symbols.map(s => ({ label: s, kind: monaco.languages.CompletionItemKind.Variable, insertText: s, range })),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── HTML COMPLETER ──────────────────────────────────────────
|
||||||
|
monaco.languages.registerCompletionItemProvider('html', {
|
||||||
|
triggerCharacters: ['<'],
|
||||||
|
provideCompletionItems: (model, position) => {
|
||||||
|
const word = model.getWordUntilPosition(position);
|
||||||
|
const range = {
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
startColumn: word.startColumn,
|
||||||
|
endColumn: word.endColumn,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: [
|
||||||
|
{ label: 'div', kind: monaco.languages.CompletionItemKind.Snippet, insertText: '<div>${1}</div>', range },
|
||||||
|
{ label: 'span', kind: monaco.languages.CompletionItemKind.Snippet, insertText: '<span>${1}</span>', range },
|
||||||
|
{ label: 'p', kind: monaco.languages.CompletionItemKind.Snippet, insertText: '<p>${1}</p>', range },
|
||||||
|
{ label: 'h1', kind: monaco.languages.CompletionItemKind.Snippet, insertText: '<h1>${1}</h1>', range },
|
||||||
|
{ label: 'button', kind: monaco.languages.CompletionItemKind.Snippet, insertText: '<button>${1:Click}</button>', range },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+13
-2
@@ -13,8 +13,8 @@ class Config:
|
|||||||
"model": "local-model",
|
"model": "local-model",
|
||||||
"api_key": "not-needed",
|
"api_key": "not-needed",
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
"max_tokens": 512,
|
"max_tokens": 4096,
|
||||||
"timeout_seconds": 5.0,
|
"timeout_seconds": 60.0,
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
@@ -60,6 +60,17 @@ class Config:
|
|||||||
if env_model:
|
if env_model:
|
||||||
config["llm"]["model"] = env_model
|
config["llm"]["model"] = env_model
|
||||||
|
|
||||||
|
env_api_key = os.getenv("TACTTERM_LLM_API_KEY")
|
||||||
|
if env_api_key:
|
||||||
|
config["llm"]["api_key"] = env_api_key
|
||||||
|
|
||||||
|
env_timeout = os.getenv("TACTTERM_LLM_TIMEOUT")
|
||||||
|
if env_timeout:
|
||||||
|
try:
|
||||||
|
config["llm"]["timeout_seconds"] = float(env_timeout)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
env_web_host = os.getenv("TACTTERM_WEB_HOST")
|
env_web_host = os.getenv("TACTTERM_WEB_HOST")
|
||||||
if env_web_host:
|
if env_web_host:
|
||||||
config["web"]["host"] = env_web_host
|
config["web"]["host"] = env_web_host
|
||||||
|
|||||||
@@ -78,12 +78,11 @@ class ChallengeGenerator:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
raw_md = (
|
msg_obj = data.get("choices", [{}])[0].get("message", {})
|
||||||
data.get("choices", [{}])[0]
|
raw_md = msg_obj.get("content") or ""
|
||||||
.get("message", {})
|
if not raw_md.strip() and msg_obj.get("reasoning_content"):
|
||||||
.get("content", "")
|
raw_md = msg_obj.get("reasoning_content", "")
|
||||||
.strip()
|
raw_md = raw_md.strip()
|
||||||
)
|
|
||||||
|
|
||||||
# Clean up outer markdown wrapper if present
|
# Clean up outer markdown wrapper if present
|
||||||
if raw_md.startswith("```markdown"):
|
if raw_md.startswith("```markdown"):
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ class HandbookService:
|
|||||||
{"role": "user", "content": prompt},
|
{"role": "user", "content": prompt},
|
||||||
],
|
],
|
||||||
"temperature": 0.2,
|
"temperature": 0.2,
|
||||||
"max_tokens": 900,
|
"max_tokens": max(config.llm_max_tokens, 4096),
|
||||||
}
|
}
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
@@ -366,12 +366,11 @@ class HandbookService:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
example_md = (
|
msg_obj = data.get("choices", [{}])[0].get("message", {})
|
||||||
data.get("choices", [{}])[0]
|
example_md = msg_obj.get("content") or ""
|
||||||
.get("message", {})
|
if not example_md.strip() and msg_obj.get("reasoning_content"):
|
||||||
.get("content", "")
|
example_md = msg_obj.get("reasoning_content", "")
|
||||||
.strip()
|
example_md = example_md.strip()
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
|
|||||||
+5
-6
@@ -53,12 +53,11 @@ class MentorClient:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
content = (
|
msg_obj = data.get("choices", [{}])[0].get("message", {})
|
||||||
data.get("choices", [{}])[0]
|
content = msg_obj.get("content") or ""
|
||||||
.get("message", {})
|
if not content.strip() and msg_obj.get("reasoning_content"):
|
||||||
.get("content", "")
|
content = msg_obj.get("reasoning_content", "")
|
||||||
.strip()
|
content = content.strip()
|
||||||
)
|
|
||||||
|
|
||||||
if content:
|
if content:
|
||||||
return {
|
return {
|
||||||
|
|||||||
+159
-20
@@ -15,7 +15,7 @@ from src.core.linter import linter
|
|||||||
from src.core.mentor import mentor
|
from src.core.mentor import mentor
|
||||||
from src.core.handbook import handbook_service
|
from src.core.handbook import handbook_service
|
||||||
from src.core.prompts import get_mentor_prompt
|
from src.core.prompts import get_mentor_prompt
|
||||||
from src.tui.widgets import CodeEditor, MentorInputTextArea, StdinInput, TUI_COMPLETIONS
|
from src.tui.widgets import CodeEditor, MentorInputTextArea, StdinInput, TUI_COMPLETIONS, get_tui_language
|
||||||
|
|
||||||
|
|
||||||
class TactiTermTUI(App):
|
class TactiTermTUI(App):
|
||||||
@@ -48,6 +48,8 @@ class TactiTermTUI(App):
|
|||||||
width: 68%;
|
width: 68%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 1;
|
padding: 0 1;
|
||||||
|
layers: default overlay;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Right columns: Mentor & Handbook sidebars (hidden by default) */
|
/* Right columns: Mentor & Handbook sidebars (hidden by default) */
|
||||||
@@ -114,7 +116,7 @@ class TactiTermTUI(App):
|
|||||||
border: solid $primary-darken-2;
|
border: solid $primary-darken-2;
|
||||||
padding: 1;
|
padding: 1;
|
||||||
background: $surface;
|
background: $surface;
|
||||||
overflow-y: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sleek compact action bar with reduced height */
|
/* Sleek compact action bar with reduced height */
|
||||||
@@ -145,11 +147,14 @@ class TactiTermTUI(App):
|
|||||||
|
|
||||||
#completion-popup {
|
#completion-popup {
|
||||||
display: none;
|
display: none;
|
||||||
height: 8;
|
layer: overlay;
|
||||||
background: $surface-darken-1;
|
position: absolute;
|
||||||
border: heavy $warning;
|
width: 32;
|
||||||
margin-bottom: 1;
|
height: auto;
|
||||||
padding: 0 1;
|
max-height: 6;
|
||||||
|
background: #252526;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#main-container.show-completion #completion-popup {
|
#main-container.show-completion #completion-popup {
|
||||||
@@ -157,14 +162,14 @@ class TactiTermTUI(App):
|
|||||||
}
|
}
|
||||||
|
|
||||||
#completion-title {
|
#completion-title {
|
||||||
height: 1;
|
display: none;
|
||||||
text-style: bold;
|
|
||||||
color: $warning;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#completion_list_popup {
|
#completion_list_popup {
|
||||||
height: 5;
|
height: 100%;
|
||||||
border: solid $warning-darken-2;
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Output Console Header & Expandable Terminal */
|
/* Output Console Header & Expandable Terminal */
|
||||||
@@ -192,7 +197,7 @@ class TactiTermTUI(App):
|
|||||||
background: $surface;
|
background: $surface;
|
||||||
border: solid $accent;
|
border: solid $accent;
|
||||||
padding: 1;
|
padding: 1;
|
||||||
overflow-y: auto;
|
overflow: auto;
|
||||||
margin-bottom: 1;
|
margin-bottom: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +232,7 @@ class TactiTermTUI(App):
|
|||||||
border: solid $warning-darken-1;
|
border: solid $warning-darken-1;
|
||||||
padding: 1;
|
padding: 1;
|
||||||
margin-bottom: 1;
|
margin-bottom: 1;
|
||||||
overflow-y: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#mentor-loading-indicator {
|
#mentor-loading-indicator {
|
||||||
@@ -266,6 +271,7 @@ class TactiTermTUI(App):
|
|||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
Binding("ctrl+r", "run_code", "Run"),
|
Binding("ctrl+r", "run_code", "Run"),
|
||||||
Binding("ctrl+l", "lint_code", "Lint"),
|
Binding("ctrl+l", "lint_code", "Lint"),
|
||||||
|
Binding("ctrl+w", "toggle_word_wrap", "Word Wrap (Ctrl+W)"),
|
||||||
Binding("ctrl+e,f4", "toggle_expand_output", "Expand Output (Ctrl+E / F4)"),
|
Binding("ctrl+e,f4", "toggle_expand_output", "Expand Output (Ctrl+E / F4)"),
|
||||||
Binding("f2", "check_answer", "Check", show=False),
|
Binding("f2", "check_answer", "Check", show=False),
|
||||||
Binding("f1", "toggle_mentor_sidebar", "Mentor", show=False),
|
Binding("f1", "toggle_mentor_sidebar", "Mentor", show=False),
|
||||||
@@ -273,6 +279,8 @@ class TactiTermTUI(App):
|
|||||||
Binding("ctrl+f", "refresh_challenges", "Refresh"),
|
Binding("ctrl+f", "refresh_challenges", "Refresh"),
|
||||||
Binding("alt+w", "scroll_output_up", "Output Up", show=False),
|
Binding("alt+w", "scroll_output_up", "Output Up", show=False),
|
||||||
Binding("alt+s", "scroll_output_down", "Output Down", show=False),
|
Binding("alt+s", "scroll_output_down", "Output Down", show=False),
|
||||||
|
Binding("alt+left,alt+a", "scroll_left", "Scroll Left", show=False),
|
||||||
|
Binding("alt+right,alt+d", "scroll_right", "Scroll Right", show=False),
|
||||||
Binding("ctrl+up", "scroll_mentor_up", "Sidebar Up", show=False),
|
Binding("ctrl+up", "scroll_mentor_up", "Sidebar Up", show=False),
|
||||||
Binding("ctrl+down", "scroll_mentor_down", "Sidebar Down", show=False),
|
Binding("ctrl+down", "scroll_mentor_down", "Sidebar Down", show=False),
|
||||||
Binding("alt+up", "scroll_details_up", "Details Up", show=False),
|
Binding("alt+up", "scroll_details_up", "Details Up", show=False),
|
||||||
@@ -312,6 +320,7 @@ class TactiTermTUI(App):
|
|||||||
yield Button("✅ Check (F2)", id="btn-check", variant="success")
|
yield Button("✅ Check (F2)", id="btn-check", variant="success")
|
||||||
yield Button("💡 Mentor (F1)", id="btn-guide", variant="warning")
|
yield Button("💡 Mentor (F1)", id="btn-guide", variant="warning")
|
||||||
yield Button("📖 Handbook (F3)", id="btn-handbook", variant="primary")
|
yield Button("📖 Handbook (F3)", id="btn-handbook", variant="primary")
|
||||||
|
yield Button("🌐 Wrap (Ctrl+W)", id="btn-wrap", variant="default")
|
||||||
yield Button("🔄 Refresh", id="btn-refresh", variant="default")
|
yield Button("🔄 Refresh", id="btn-refresh", variant="default")
|
||||||
|
|
||||||
yield CodeEditor(id="editor")
|
yield CodeEditor(id="editor")
|
||||||
@@ -448,6 +457,8 @@ class TactiTermTUI(App):
|
|||||||
self.action_toggle_handbook_sidebar()
|
self.action_toggle_handbook_sidebar()
|
||||||
elif button_id == "btn-handbook-mode":
|
elif button_id == "btn-handbook-mode":
|
||||||
self.toggle_handbook_mode()
|
self.toggle_handbook_mode()
|
||||||
|
elif button_id == "btn-wrap":
|
||||||
|
self.action_toggle_word_wrap()
|
||||||
elif button_id == "btn-expand-output":
|
elif button_id == "btn-expand-output":
|
||||||
self.toggle_expand_output()
|
self.toggle_expand_output()
|
||||||
elif button_id == "btn-send-stdin":
|
elif button_id == "btn-send-stdin":
|
||||||
@@ -646,7 +657,7 @@ class TactiTermTUI(App):
|
|||||||
self.query_one("#challenge-details", Markdown).update(markdown_content)
|
self.query_one("#challenge-details", Markdown).update(markdown_content)
|
||||||
|
|
||||||
editor = self.query_one("#editor", CodeEditor)
|
editor = self.query_one("#editor", CodeEditor)
|
||||||
lang_key = challenge.language.lower()
|
lang_key = get_tui_language(challenge.language)
|
||||||
try:
|
try:
|
||||||
if lang_key in editor.available_languages:
|
if lang_key in editor.available_languages:
|
||||||
editor.language = lang_key
|
editor.language = lang_key
|
||||||
@@ -690,9 +701,7 @@ class TactiTermTUI(App):
|
|||||||
editor.delete((cursor_row, start_col), (cursor_row, cursor_col))
|
editor.delete((cursor_row, start_col), (cursor_row, cursor_col))
|
||||||
editor.insert(chosen_text)
|
editor.insert(chosen_text)
|
||||||
|
|
||||||
main_box = self.query_one("#main-container")
|
self.hide_completion_popup()
|
||||||
main_box.remove_class("show-completion")
|
|
||||||
editor.focus()
|
|
||||||
self.query_one("#status-bar", Static).update(f"✓ Inserted '{chosen_text}'")
|
self.query_one("#status-bar", Static).update(f"✓ Inserted '{chosen_text}'")
|
||||||
elif event.option_list.id == "handbook_topic_list":
|
elif event.option_list.id == "handbook_topic_list":
|
||||||
selected_option = event.option
|
selected_option = event.option
|
||||||
@@ -708,7 +717,63 @@ class TactiTermTUI(App):
|
|||||||
)
|
)
|
||||||
self.run_worker(self._handbook_worker(lang, item["id"], item["title"]))
|
self.run_worker(self._handbook_worker(lang, item["id"], item["title"]))
|
||||||
|
|
||||||
def show_completion_popup(self, prefix: str, matches: List[str]) -> None:
|
def is_completion_open(self) -> bool:
|
||||||
|
"""Returns True if completion popup overlay is visible."""
|
||||||
|
main_box = self.query_one("#main-container")
|
||||||
|
return main_box.has_class("show-completion")
|
||||||
|
|
||||||
|
def hide_completion_popup(self) -> None:
|
||||||
|
"""Dismiss completion popup menu."""
|
||||||
|
main_box = self.query_one("#main-container")
|
||||||
|
if main_box.has_class("show-completion"):
|
||||||
|
main_box.remove_class("show-completion")
|
||||||
|
|
||||||
|
def update_as_you_type_completion(self, editor: CodeEditor) -> None:
|
||||||
|
"""Real-time buffer listener to update popup matches as user types or backspaces."""
|
||||||
|
prefix, matches, is_dot = editor.get_completions_at_cursor()
|
||||||
|
if matches:
|
||||||
|
self.show_completion_popup(prefix, matches, is_dot)
|
||||||
|
elif self.is_completion_open():
|
||||||
|
self.hide_completion_popup()
|
||||||
|
|
||||||
|
def navigate_completion(self, direction: int) -> None:
|
||||||
|
"""Navigate highlighted item in floating completion list."""
|
||||||
|
if not self.is_completion_open():
|
||||||
|
return
|
||||||
|
popup_list = self.query_one("#completion_list_popup", OptionList)
|
||||||
|
if len(popup_list.options) > 0:
|
||||||
|
current = popup_list.highlighted if popup_list.highlighted is not None else 0
|
||||||
|
popup_list.highlighted = (current + direction) % len(popup_list.options)
|
||||||
|
popup_list.scroll_to_highlight()
|
||||||
|
|
||||||
|
def insert_selected_completion(self) -> None:
|
||||||
|
"""Insert currently highlighted completion option into CodeEditor, cleanly replacing typed token."""
|
||||||
|
if not self.is_completion_open():
|
||||||
|
return
|
||||||
|
popup_list = self.query_one("#completion_list_popup", OptionList)
|
||||||
|
if popup_list.highlighted is not None and popup_list.highlighted < len(popup_list.options):
|
||||||
|
option = popup_list.get_option_at_index(popup_list.highlighted)
|
||||||
|
chosen_text = str(option.prompt)
|
||||||
|
editor = self.query_one("#editor", CodeEditor)
|
||||||
|
cursor_row, cursor_col = editor.cursor_location
|
||||||
|
lines = editor.text.split("\n")
|
||||||
|
line_until_cursor = lines[cursor_row][:cursor_col] if cursor_row < len(lines) else ""
|
||||||
|
|
||||||
|
import re
|
||||||
|
m_dot = re.search(r"([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z0-9_]*)$", line_until_cursor)
|
||||||
|
if m_dot:
|
||||||
|
typed_len = len(m_dot.group(2))
|
||||||
|
else:
|
||||||
|
m_word = re.search(r"([a-zA-Z_][a-zA-Z0-9_]*)$", line_until_cursor)
|
||||||
|
typed_len = len(m_word.group(1)) if m_word else len(self.active_completion_prefix)
|
||||||
|
|
||||||
|
start_col = max(0, cursor_col - typed_len)
|
||||||
|
editor.delete((cursor_row, start_col), (cursor_row, cursor_col))
|
||||||
|
editor.insert(chosen_text)
|
||||||
|
self.hide_completion_popup()
|
||||||
|
self.query_one("#status-bar", Static).update(f"✓ Inserted '{chosen_text}'")
|
||||||
|
|
||||||
|
def show_completion_popup(self, prefix: str, matches: List[str], is_dot_access: bool = False) -> None:
|
||||||
self.active_completion_prefix = prefix
|
self.active_completion_prefix = prefix
|
||||||
popup_list = self.query_one("#completion_list_popup", OptionList)
|
popup_list = self.query_one("#completion_list_popup", OptionList)
|
||||||
popup_list.clear_options()
|
popup_list.clear_options()
|
||||||
@@ -716,9 +781,48 @@ class TactiTermTUI(App):
|
|||||||
for match in matches:
|
for match in matches:
|
||||||
popup_list.add_option(Option(match, id=match))
|
popup_list.add_option(Option(match, id=match))
|
||||||
|
|
||||||
|
editor = self.query_one("#editor", CodeEditor)
|
||||||
|
popup = self.query_one("#completion-popup", Container)
|
||||||
|
panel = popup.parent or editor.parent
|
||||||
|
|
||||||
|
cursor_row, cursor_col = editor.cursor_location
|
||||||
|
gutter = getattr(editor, "gutter_width", 3)
|
||||||
|
vis_row = cursor_row - int(getattr(editor, "scroll_y", 0))
|
||||||
|
vis_col = cursor_col - int(getattr(editor, "scroll_x", 0))
|
||||||
|
|
||||||
|
popup_height = max(1, min(len(matches), 5))
|
||||||
|
popup.styles.height = popup_height
|
||||||
|
|
||||||
|
try:
|
||||||
|
editor_top_rel = editor.region.y - panel.region.y
|
||||||
|
editor_left_rel = editor.region.x - panel.region.x
|
||||||
|
editor_height = editor.region.height
|
||||||
|
panel_width = panel.region.width
|
||||||
|
except Exception:
|
||||||
|
editor_top_rel = 2
|
||||||
|
editor_left_rel = 0
|
||||||
|
editor_height = 15
|
||||||
|
panel_width = 80
|
||||||
|
|
||||||
|
# Line Y position relative to panel (1 cell for top border of editor)
|
||||||
|
line_y = editor_top_rel + 1 + vis_row
|
||||||
|
|
||||||
|
# Smart placement: position BELOW cursor line if space permits, else ABOVE cursor line
|
||||||
|
if line_y + 1 + popup_height <= editor_top_rel + editor_height:
|
||||||
|
top = line_y + 1
|
||||||
|
else:
|
||||||
|
top = max(editor_top_rel + 1, line_y - popup_height)
|
||||||
|
|
||||||
|
# X position: editor left rel + 1 for border + gutter + vis_col
|
||||||
|
left_pos = editor_left_rel + 1 + gutter + vis_col
|
||||||
|
popup_width = 32
|
||||||
|
max_left = max(0, panel_width - popup_width)
|
||||||
|
left = min(max(0, left_pos), max_left)
|
||||||
|
|
||||||
|
popup.styles.offset = (int(left), int(top))
|
||||||
|
|
||||||
main_box = self.query_one("#main-container")
|
main_box = self.query_one("#main-container")
|
||||||
main_box.add_class("show-completion")
|
main_box.add_class("show-completion")
|
||||||
popup_list.focus()
|
|
||||||
if len(matches) > 0:
|
if len(matches) > 0:
|
||||||
popup_list.highlighted = 0
|
popup_list.highlighted = 0
|
||||||
|
|
||||||
@@ -744,6 +848,41 @@ class TactiTermTUI(App):
|
|||||||
def action_scroll_output_down(self) -> None:
|
def action_scroll_output_down(self) -> None:
|
||||||
self.query_one("#output-scroll-container", VerticalScroll).scroll_down(animate=False)
|
self.query_one("#output-scroll-container", VerticalScroll).scroll_down(animate=False)
|
||||||
|
|
||||||
|
def action_toggle_word_wrap(self) -> None:
|
||||||
|
"""Toggle soft word wrapping on CodeEditor."""
|
||||||
|
editor = self.query_one("#editor", CodeEditor)
|
||||||
|
editor.soft_wrap = not editor.soft_wrap
|
||||||
|
status = "ON" if editor.soft_wrap else "OFF"
|
||||||
|
try:
|
||||||
|
btn = self.query_one("#btn-wrap", Button)
|
||||||
|
if editor.soft_wrap:
|
||||||
|
btn.label = "🌐 Wrap: ON"
|
||||||
|
btn.variant = "warning"
|
||||||
|
else:
|
||||||
|
btn.label = "🌐 Wrap: OFF"
|
||||||
|
btn.variant = "default"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.notify(f"Code Editor Word Wrap turned {status}", title="Word Wrap Toggled")
|
||||||
|
|
||||||
|
def action_scroll_left(self) -> None:
|
||||||
|
"""Scroll active container or detail/output panels left horizontally."""
|
||||||
|
focused = self.focused
|
||||||
|
if focused and hasattr(focused, "scroll_left"):
|
||||||
|
focused.scroll_left(animate=False)
|
||||||
|
else:
|
||||||
|
self.query_one("#challenge-details-container", VerticalScroll).scroll_left(animate=False)
|
||||||
|
self.query_one("#output-scroll-container", VerticalScroll).scroll_left(animate=False)
|
||||||
|
|
||||||
|
def action_scroll_right(self) -> None:
|
||||||
|
"""Scroll active container or detail/output panels right horizontally."""
|
||||||
|
focused = self.focused
|
||||||
|
if focused and hasattr(focused, "scroll_right"):
|
||||||
|
focused.scroll_right(animate=False)
|
||||||
|
else:
|
||||||
|
self.query_one("#challenge-details-container", VerticalScroll).scroll_right(animate=False)
|
||||||
|
self.query_one("#output-scroll-container", VerticalScroll).scroll_right(animate=False)
|
||||||
|
|
||||||
def action_scroll_details_up(self) -> None:
|
def action_scroll_details_up(self) -> None:
|
||||||
self.query_one("#challenge-details-container", VerticalScroll).scroll_up(animate=False)
|
self.query_one("#challenge-details-container", VerticalScroll).scroll_up(animate=False)
|
||||||
|
|
||||||
|
|||||||
+11
-2
@@ -141,7 +141,7 @@ class GenHelpModal(ModalScreen):
|
|||||||
},
|
},
|
||||||
{"role": "user", "content": question},
|
{"role": "user", "content": question},
|
||||||
],
|
],
|
||||||
"max_tokens": 1000,
|
"max_tokens": config.llm_max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
@@ -234,7 +234,7 @@ class GenTUIApp(App):
|
|||||||
border: solid $secondary;
|
border: solid $secondary;
|
||||||
padding: 1;
|
padding: 1;
|
||||||
background: $surface;
|
background: $surface;
|
||||||
overflow-y: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#btn-help-modal {
|
#btn-help-modal {
|
||||||
@@ -262,6 +262,7 @@ class GenTUIApp(App):
|
|||||||
Binding("ctrl+h", "open_help_modal", "AI Helper"),
|
Binding("ctrl+h", "open_help_modal", "AI Helper"),
|
||||||
Binding("ctrl+g", "generate_ai", "Generate AI"),
|
Binding("ctrl+g", "generate_ai", "Generate AI"),
|
||||||
Binding("ctrl+s", "save_challenge", "Save File"),
|
Binding("ctrl+s", "save_challenge", "Save File"),
|
||||||
|
Binding("ctrl+w", "toggle_word_wrap", "Word Wrap (Ctrl+W)"),
|
||||||
Binding("ctrl+q", "quit", "Quit"),
|
Binding("ctrl+q", "quit", "Quit"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -404,6 +405,14 @@ class GenTUIApp(App):
|
|||||||
else:
|
else:
|
||||||
self.query_one("#status-bar", Static).update(f"✗ Save Error: {res.get('message')}")
|
self.query_one("#status-bar", Static).update(f"✗ Save Error: {res.get('message')}")
|
||||||
|
|
||||||
|
def action_toggle_word_wrap(self) -> None:
|
||||||
|
"""Toggle soft word wrap in the raw markdown editor."""
|
||||||
|
editor = self.query_one("#markdown-editor", TextArea)
|
||||||
|
editor.soft_wrap = not editor.soft_wrap
|
||||||
|
status = "ON" if editor.soft_wrap else "OFF"
|
||||||
|
self.notify(f"Markdown Editor Word Wrap turned {status}", title="Word Wrap Toggled")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="GenTUI — TactiTerm AI Challenge Generator")
|
parser = argparse.ArgumentParser(description="GenTUI — TactiTerm AI Challenge Generator")
|
||||||
|
|||||||
+287
-19
@@ -79,6 +79,217 @@ TUI_COMPLETIONS: Dict[str, List[str]] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Duplicate csharp completion list for c_sharp key
|
||||||
|
TUI_COMPLETIONS["c_sharp"] = TUI_COMPLETIONS["csharp"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_tui_language(language: str) -> str:
|
||||||
|
"""Map display/challenge language names to Textual's expected TextArea language keys."""
|
||||||
|
if not language:
|
||||||
|
return "python"
|
||||||
|
norm = language.lower().strip()
|
||||||
|
mapping = {
|
||||||
|
"c++": "cpp",
|
||||||
|
"cpp": "cpp",
|
||||||
|
"cxx": "cpp",
|
||||||
|
"c#": "c_sharp",
|
||||||
|
"cs": "c_sharp",
|
||||||
|
"csharp": "c_sharp",
|
||||||
|
"c_sharp": "c_sharp",
|
||||||
|
"js": "javascript",
|
||||||
|
"javascript": "javascript",
|
||||||
|
"ts": "typescript",
|
||||||
|
"typescript": "typescript",
|
||||||
|
"py": "python",
|
||||||
|
"python": "python",
|
||||||
|
"rs": "rust",
|
||||||
|
"rust": "rust",
|
||||||
|
"golang": "go",
|
||||||
|
"go": "go",
|
||||||
|
"html": "html",
|
||||||
|
"java": "java",
|
||||||
|
"lua": "lua",
|
||||||
|
}
|
||||||
|
return mapping.get(norm, norm)
|
||||||
|
|
||||||
|
|
||||||
|
def register_custom_tree_sitter_languages(editor: TextArea) -> None:
|
||||||
|
"""Register tree-sitter language grammars with rich query definitions for cpp, c_sharp, typescript, lua if available."""
|
||||||
|
langs = {
|
||||||
|
"cpp": (
|
||||||
|
"tree_sitter_cpp",
|
||||||
|
"language",
|
||||||
|
[
|
||||||
|
'["if" "else" "for" "while" "return" "class" "struct" "namespace" "using" "public" "private" "protected" "virtual" "const" "inline" "static" "template" "typename" "new" "delete" "catch" "try" "throw"] @keyword',
|
||||||
|
'(primitive_type) @type',
|
||||||
|
'(type_identifier) @type',
|
||||||
|
'(comment) @comment',
|
||||||
|
'(string_literal) @string',
|
||||||
|
'(system_lib_string) @string',
|
||||||
|
'(number_literal) @number',
|
||||||
|
'(field_identifier) @property',
|
||||||
|
'(function_declarator declarator: (identifier) @function)',
|
||||||
|
'(call_expression function: (identifier) @function.call)',
|
||||||
|
'(preproc_include) @include',
|
||||||
|
'(preproc_def) @keyword',
|
||||||
|
'(true) @boolean',
|
||||||
|
'(false) @boolean',
|
||||||
|
'(null) @constant.builtin',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"c_sharp": (
|
||||||
|
"tree_sitter_c_sharp",
|
||||||
|
"language",
|
||||||
|
[
|
||||||
|
'["if" "else" "for" "foreach" "while" "return" "class" "struct" "interface" "public" "private" "protected" "internal" "static" "async" "await" "using" "namespace" "new" "get" "set" "try" "catch" "throw"] @keyword',
|
||||||
|
'(predefined_type) @type',
|
||||||
|
'(comment) @comment',
|
||||||
|
'(string_literal) @string',
|
||||||
|
'(verbatim_string_literal) @string',
|
||||||
|
'(integer_literal) @number',
|
||||||
|
'(real_literal) @number',
|
||||||
|
'(identifier) @variable',
|
||||||
|
'(method_declaration name: (identifier) @function)',
|
||||||
|
'(invocation_expression function: (identifier) @function.call)',
|
||||||
|
'(boolean_literal) @boolean',
|
||||||
|
'(null_literal) @constant.builtin',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"typescript": (
|
||||||
|
"tree_sitter_typescript",
|
||||||
|
"language_typescript",
|
||||||
|
[
|
||||||
|
'["if" "else" "for" "while" "return" "function" "class" "interface" "type" "const" "let" "var" "import" "from" "export" "async" "await" "new" "try" "catch" "throw" "switch" "case"] @keyword',
|
||||||
|
'(predefined_type) @type',
|
||||||
|
'(type_identifier) @type',
|
||||||
|
'(comment) @comment',
|
||||||
|
'(string) @string',
|
||||||
|
'(template_string) @string',
|
||||||
|
'(number) @number',
|
||||||
|
'(property_identifier) @property',
|
||||||
|
'(function_declaration name: (identifier) @function)',
|
||||||
|
'(call_expression function: (identifier) @function.call)',
|
||||||
|
'(true) @boolean',
|
||||||
|
'(false) @boolean',
|
||||||
|
'(null) @constant.builtin',
|
||||||
|
'(undefined) @constant.builtin',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"lua": (
|
||||||
|
"tree_sitter_lua",
|
||||||
|
"language",
|
||||||
|
[
|
||||||
|
'["if" "then" "else" "elseif" "end" "function" "return" "while" "for" "do" "local" "repeat" "until" "and" "or" "not" "in"] @keyword',
|
||||||
|
'(break_statement) @keyword',
|
||||||
|
'(comment) @comment',
|
||||||
|
'(string) @string',
|
||||||
|
'(number) @number',
|
||||||
|
'(function_declaration name: (identifier) @function)',
|
||||||
|
'(function_call name: (identifier) @function.call)',
|
||||||
|
'(dot_index_expression field: (identifier) @property)',
|
||||||
|
'(true) @boolean',
|
||||||
|
'(false) @boolean',
|
||||||
|
'(nil) @constant.builtin',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for key, (mod_name, func_name, sample_queries) in langs.items():
|
||||||
|
if key not in editor.available_languages:
|
||||||
|
try:
|
||||||
|
import importlib
|
||||||
|
import textual._tree_sitter as ts
|
||||||
|
import textual.widgets._text_area as ta
|
||||||
|
|
||||||
|
mod = importlib.import_module(mod_name)
|
||||||
|
func = getattr(mod, func_name)
|
||||||
|
lang_obj = ts.Language(func())
|
||||||
|
|
||||||
|
query_str = ""
|
||||||
|
for q in sample_queries:
|
||||||
|
try:
|
||||||
|
document = ta.SyntaxAwareDocument("", lang_obj)
|
||||||
|
document.prepare_query(q)
|
||||||
|
query_str += ("\n" if query_str else "") + q
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
editor.register_language(key, lang_obj, query_str)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
class CodebaseSymbolExtractor:
|
||||||
|
"""Extracts symbols, variables, functions, and classes from the active code buffer."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def extract_symbols(code: str) -> List[str]:
|
||||||
|
if not code:
|
||||||
|
return []
|
||||||
|
words = re.findall(r"[a-zA-Z_][a-zA-Z0-9_]*", code)
|
||||||
|
unique = set()
|
||||||
|
reserved = {
|
||||||
|
"if", "else", "for", "while", "return", "import", "from", "def", "class",
|
||||||
|
"public", "private", "protected", "static", "void", "int", "double", "float",
|
||||||
|
"bool", "boolean", "char", "const", "let", "var", "func", "package", "struct",
|
||||||
|
}
|
||||||
|
for w in words:
|
||||||
|
if len(w) > 2 and w not in reserved:
|
||||||
|
unique.add(w)
|
||||||
|
return sorted(list(unique))
|
||||||
|
|
||||||
|
|
||||||
|
METHOD_CATALOGS: Dict[str, Dict[str, List[str]]] = {
|
||||||
|
"java": {
|
||||||
|
"map": ["put(key, value)", "get(key)", "containsKey(key)", "containsValue(val)", "size()", "isEmpty()", "keySet()", "values()", "entrySet()", "remove(key)", "clear()", "getOrDefault(key, default)"],
|
||||||
|
"list": ["add(element)", "get(index)", "size()", "remove(index)", "contains(element)", "indexOf(element)", "isEmpty()", "clear()", "set(index, element)"],
|
||||||
|
"set": ["add(element)", "remove(element)", "contains(element)", "size()", "isEmpty()", "clear()"],
|
||||||
|
"string": ["length()", "substring(beginIndex)", "charAt(index)", "toLowerCase()", "toUpperCase()", "trim()", "split(regex)", "contains(str)", "startsWith(prefix)", "equals(obj)"],
|
||||||
|
"system": ["println(value)", "printf(format, args)", "print(value)"],
|
||||||
|
},
|
||||||
|
"python": {
|
||||||
|
"map": ["get(key)", "keys()", "values()", "items()", "update(dict)", "pop(key)", "clear()"],
|
||||||
|
"list": ["append(item)", "extend(iterable)", "insert(index, item)", "remove(item)", "pop()", "sort()", "reverse()", "clear()", "count(item)", "index(item)"],
|
||||||
|
"string": ["split(sep)", "join(iterable)", "lower()", "upper()", "strip()", "replace(old, new)", "startswith(prefix)", "endswith(suffix)", "find(sub)"],
|
||||||
|
},
|
||||||
|
"cpp": {
|
||||||
|
"map": ["insert({key, val})", "find(key)", "count(key)", "size()", "empty()", "clear()", "at(key)"],
|
||||||
|
"list": ["push_back(val)", "pop_back()", "size()", "empty()", "clear()", "begin()", "end()", "at(idx)"],
|
||||||
|
"string": ["length()", "size()", "substr(pos, len)", "append(str)", "find(str)", "c_str()", "empty()"],
|
||||||
|
"std": ["cout << value << std::endl;", "cin >> var;", "vector<int>", "map<string, int>", "sort(begin, end)"],
|
||||||
|
},
|
||||||
|
"c_sharp": {
|
||||||
|
"map": ["Add(key, val)", "ContainsKey(key)", "TryGetValue(key, out val)", "Remove(key)", "Count", "Clear()"],
|
||||||
|
"list": ["Add(item)", "Remove(item)", "RemoveAt(index)", "Contains(item)", "Count", "Clear()"],
|
||||||
|
"string": ["Length", "Substring(startIndex)", "ToLower()", "ToUpper()", "Trim()", "Split(sep)", "Replace(old, new)"],
|
||||||
|
"console": ["WriteLine(value)", "ReadLine()", "Write(value)"],
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"map": ["set(key, val)", "get(key)", "has(key)", "delete(key)", "clear()", "size"],
|
||||||
|
"list": ["map(x => x)", "filter(x => true)", "push(item)", "pop()", "slice(start, end)", "includes(item)", "length"],
|
||||||
|
"console": ["log(msg)", "error(err)", "warn(msg)"],
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"map": ["set(key, val)", "get(key)", "has(key)", "delete(key)", "clear()", "size"],
|
||||||
|
"list": ["map(x => x)", "filter(x => true)", "push(item)", "pop()", "slice(start, end)", "includes(item)", "length"],
|
||||||
|
"console": ["log(msg)", "error(err)", "warn(msg)"],
|
||||||
|
},
|
||||||
|
"rust": {
|
||||||
|
"map": ["insert(key, val)", "get(&key)", "contains_key(&key)", "remove(&key)", "len()", "is_empty()"],
|
||||||
|
"list": ["push(val)", "pop()", "len()", "is_empty()", "contains(&val)", "iter()", "collect()"],
|
||||||
|
},
|
||||||
|
"go": {
|
||||||
|
"fmt": ["Println(v)", "Printf(format, v)", "Sprintf(format, v)"],
|
||||||
|
"strings": ["Split(s, sep)", "Join(a, sep)", "ToLower(s)", "ToUpper(s)"],
|
||||||
|
},
|
||||||
|
"lua": {
|
||||||
|
"table": ["insert(t, val)", "remove(t, pos)", "concat(t, sep)", "sort(t)"],
|
||||||
|
"string": ["sub(s, i, j)", "lower(s)", "upper(s)", "len(s)"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CodeEditor(TextArea):
|
class CodeEditor(TextArea):
|
||||||
"""A code editor with auto-completion dropdown trigger."""
|
"""A code editor with auto-completion dropdown trigger."""
|
||||||
|
|
||||||
@@ -101,13 +312,15 @@ class CodeEditor(TextArea):
|
|||||||
classes: str | None = None,
|
classes: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
language=language,
|
language=None,
|
||||||
theme=theme,
|
theme=theme,
|
||||||
soft_wrap=False,
|
soft_wrap=False,
|
||||||
show_line_numbers=True,
|
show_line_numbers=True,
|
||||||
id=id,
|
id=id,
|
||||||
classes=classes,
|
classes=classes,
|
||||||
)
|
)
|
||||||
|
register_custom_tree_sitter_languages(self)
|
||||||
|
self.language = get_tui_language(language)
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
self.tab_behavior = "indent"
|
self.tab_behavior = "indent"
|
||||||
@@ -116,8 +329,60 @@ class CodeEditor(TextArea):
|
|||||||
def get_code(self) -> str:
|
def get_code(self) -> str:
|
||||||
return self.text
|
return self.text
|
||||||
|
|
||||||
|
def get_completions_at_cursor(self) -> tuple[str, List[str], bool]:
|
||||||
|
"""Returns (prefix_to_replace, matching_candidates, is_dot_access)."""
|
||||||
|
cursor_row, cursor_col = self.cursor_location
|
||||||
|
lines = self.text.split("\n")
|
||||||
|
if cursor_row >= len(lines):
|
||||||
|
return ("", [], False)
|
||||||
|
|
||||||
|
current_line = lines[cursor_row][:cursor_col]
|
||||||
|
lang = get_tui_language(self.language or "python")
|
||||||
|
|
||||||
|
# 1. Check dot member access (e.g. stockMap. or stockMap.p or System.out.)
|
||||||
|
dot_match = re.search(r"([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z0-9_]*)$", current_line)
|
||||||
|
if dot_match:
|
||||||
|
obj_name = dot_match.group(1)
|
||||||
|
member_prefix = dot_match.group(2)
|
||||||
|
obj_lower = obj_name.lower()
|
||||||
|
|
||||||
|
lang_methods = METHOD_CATALOGS.get(lang, METHOD_CATALOGS.get(lang.replace("_", ""), METHOD_CATALOGS["java"]))
|
||||||
|
target_type = "map"
|
||||||
|
if "map" in obj_lower or "dict" in obj_lower:
|
||||||
|
target_type = "map"
|
||||||
|
elif "list" in obj_lower or "arr" in obj_lower or "items" in obj_lower:
|
||||||
|
target_type = "list"
|
||||||
|
elif "set" in obj_lower:
|
||||||
|
target_type = "set"
|
||||||
|
elif "str" in obj_lower or "text" in obj_lower or "name" in obj_lower or "msg" in obj_lower:
|
||||||
|
target_type = "string"
|
||||||
|
elif obj_lower in lang_methods:
|
||||||
|
target_type = obj_lower
|
||||||
|
|
||||||
|
candidates = lang_methods.get(target_type, lang_methods.get("map", []))
|
||||||
|
matches = [c for c in candidates if c.lower().startswith(member_prefix.lower())]
|
||||||
|
return (member_prefix, matches, True)
|
||||||
|
|
||||||
|
# 2. Standalone prefix word completion
|
||||||
|
word_match = re.search(r"([a-zA-Z_][a-zA-Z0-9_]*)$", current_line)
|
||||||
|
if word_match:
|
||||||
|
prefix = word_match.group(1)
|
||||||
|
if len(prefix) >= 1:
|
||||||
|
static_candidates = TUI_COMPLETIONS.get(lang, TUI_COMPLETIONS.get(lang.replace("_", ""), TUI_COMPLETIONS["python"]))
|
||||||
|
extracted_symbols = CodebaseSymbolExtractor.extract_symbols(self.text)
|
||||||
|
all_candidates = list(dict.fromkeys(static_candidates + extracted_symbols))
|
||||||
|
matches = [c for c in all_candidates if c.lower().startswith(prefix.lower()) and c.lower() != prefix.lower()]
|
||||||
|
return (prefix, matches, False)
|
||||||
|
|
||||||
|
return ("", [], False)
|
||||||
|
|
||||||
|
def on_text_area_changed(self, event: TextArea.Changed) -> None:
|
||||||
|
"""Trigger instant as-you-type completion update on any text mutation."""
|
||||||
|
if hasattr(self.app, "update_as_you_type_completion"):
|
||||||
|
self.app.update_as_you_type_completion(self)
|
||||||
|
|
||||||
def _on_key(self, event: events.Key) -> None:
|
def _on_key(self, event: events.Key) -> None:
|
||||||
"""Handle Tab completion and Ctrl+E toggle."""
|
"""Handle completion key navigation, selection, escape, and Ctrl+E toggle."""
|
||||||
if event.key == "ctrl+e":
|
if event.key == "ctrl+e":
|
||||||
event.prevent_default()
|
event.prevent_default()
|
||||||
event.stop()
|
event.stop()
|
||||||
@@ -125,24 +390,27 @@ class CodeEditor(TextArea):
|
|||||||
self.app.action_toggle_expand_output()
|
self.app.action_toggle_expand_output()
|
||||||
return
|
return
|
||||||
|
|
||||||
if event.key == "tab":
|
is_open = getattr(self.app, "is_completion_open", lambda: False)()
|
||||||
cursor_row, cursor_col = self.cursor_location
|
|
||||||
lines = self.text.split("\n")
|
|
||||||
if cursor_row < len(lines):
|
|
||||||
current_line = lines[cursor_row][:cursor_col]
|
|
||||||
words = current_line.replace("(", " ").replace(")", " ").split()
|
|
||||||
if words:
|
|
||||||
prefix = words[-1].lower()
|
|
||||||
lang = (self.language or "python").lower()
|
|
||||||
candidates = TUI_COMPLETIONS.get(lang, TUI_COMPLETIONS["python"])
|
|
||||||
matches = [c for c in candidates if c.lower().startswith(prefix) or prefix in c.lower()]
|
|
||||||
|
|
||||||
if matches:
|
if is_open:
|
||||||
event.prevent_default()
|
if event.key in ("down", "up"):
|
||||||
event.stop()
|
event.prevent_default()
|
||||||
if hasattr(self.app, "show_completion_popup"):
|
event.stop()
|
||||||
self.app.show_completion_popup(words[-1], matches)
|
if hasattr(self.app, "navigate_completion"):
|
||||||
return
|
self.app.navigate_completion(1 if event.key == "down" else -1)
|
||||||
|
return
|
||||||
|
elif event.key in ("enter", "tab"):
|
||||||
|
event.prevent_default()
|
||||||
|
event.stop()
|
||||||
|
if hasattr(self.app, "insert_selected_completion"):
|
||||||
|
self.app.insert_selected_completion()
|
||||||
|
return
|
||||||
|
elif event.key == "escape":
|
||||||
|
event.prevent_default()
|
||||||
|
event.stop()
|
||||||
|
if hasattr(self.app, "hide_completion_popup"):
|
||||||
|
self.app.hide_completion_popup()
|
||||||
|
return
|
||||||
|
|
||||||
super()._on_key(event)
|
super()._on_key(event)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user