Neovim improvements: - Add tree-sitter text objects for functions, types, blocks - Add folding support - Enhanced REPL integration (toggle, send line/selection) - New commands: LuxCheck, LuxReplToggle, LuxSend - Better keybindings with localleader VS Code extension: - Full syntax highlighting with TextMate grammar - LSP client integration - 20+ snippets for common patterns - Commands: run, format, check, REPL - Keybindings and context menu Fixes: - Fix all cargo warnings with #[allow(dead_code)] annotations - Clean up unused variables Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
242 lines
6.9 KiB
TypeScript
242 lines
6.9 KiB
TypeScript
import * as vscode from 'vscode';
|
|
import * as path from 'path';
|
|
import { spawn, ChildProcess } from 'child_process';
|
|
import {
|
|
LanguageClient,
|
|
LanguageClientOptions,
|
|
ServerOptions,
|
|
} from 'vscode-languageclient/node';
|
|
|
|
let client: LanguageClient | undefined;
|
|
let replTerminal: vscode.Terminal | undefined;
|
|
|
|
export function activate(context: vscode.ExtensionContext) {
|
|
console.log('Lux extension activated');
|
|
|
|
// Start LSP if enabled
|
|
const config = vscode.workspace.getConfiguration('lux');
|
|
if (config.get('lsp.enabled', true)) {
|
|
startLspClient(context);
|
|
}
|
|
|
|
// Register commands
|
|
context.subscriptions.push(
|
|
vscode.commands.registerCommand('lux.run', runCurrentFile),
|
|
vscode.commands.registerCommand('lux.format', formatDocument),
|
|
vscode.commands.registerCommand('lux.check', checkFile),
|
|
vscode.commands.registerCommand('lux.startRepl', startRepl),
|
|
vscode.commands.registerCommand('lux.sendToRepl', sendToRepl),
|
|
vscode.commands.registerCommand('lux.restartLsp', () => restartLspClient(context))
|
|
);
|
|
|
|
// Format on save if enabled
|
|
if (config.get('format.onSave', false)) {
|
|
context.subscriptions.push(
|
|
vscode.workspace.onWillSaveTextDocument((event) => {
|
|
if (event.document.languageId === 'lux') {
|
|
event.waitUntil(formatDocumentAsync(event.document));
|
|
}
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
export function deactivate(): Thenable<void> | undefined {
|
|
if (replTerminal) {
|
|
replTerminal.dispose();
|
|
}
|
|
if (client) {
|
|
return client.stop();
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function getLuxBinary(): string {
|
|
const config = vscode.workspace.getConfiguration('lux');
|
|
const customPath = config.get<string>('lspPath');
|
|
|
|
if (customPath && customPath.trim() !== '') {
|
|
return customPath;
|
|
}
|
|
|
|
// Try common locations
|
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
|
const possiblePaths = [
|
|
workspaceFolder ? path.join(workspaceFolder, 'result', 'bin', 'lux') : null,
|
|
workspaceFolder ? path.join(workspaceFolder, 'target', 'release', 'lux') : null,
|
|
workspaceFolder ? path.join(workspaceFolder, 'target', 'debug', 'lux') : null,
|
|
].filter(Boolean) as string[];
|
|
|
|
// Check if any exist
|
|
for (const p of possiblePaths) {
|
|
try {
|
|
require('fs').accessSync(p, require('fs').constants.X_OK);
|
|
return p;
|
|
} catch {
|
|
// Continue to next
|
|
}
|
|
}
|
|
|
|
// Fall back to PATH
|
|
return 'lux';
|
|
}
|
|
|
|
function startLspClient(context: vscode.ExtensionContext) {
|
|
const luxBinary = getLuxBinary();
|
|
|
|
const serverOptions: ServerOptions = {
|
|
command: luxBinary,
|
|
args: ['lsp'],
|
|
};
|
|
|
|
const clientOptions: LanguageClientOptions = {
|
|
documentSelector: [{ scheme: 'file', language: 'lux' }],
|
|
synchronize: {
|
|
fileEvents: vscode.workspace.createFileSystemWatcher('**/*.lux'),
|
|
},
|
|
};
|
|
|
|
client = new LanguageClient(
|
|
'luxLanguageServer',
|
|
'Lux Language Server',
|
|
serverOptions,
|
|
clientOptions
|
|
);
|
|
|
|
client.start();
|
|
}
|
|
|
|
async function restartLspClient(context: vscode.ExtensionContext) {
|
|
if (client) {
|
|
await client.stop();
|
|
}
|
|
startLspClient(context);
|
|
vscode.window.showInformationMessage('Lux language server restarted');
|
|
}
|
|
|
|
async function runCurrentFile() {
|
|
const editor = vscode.window.activeTextEditor;
|
|
if (!editor || editor.document.languageId !== 'lux') {
|
|
vscode.window.showErrorMessage('No Lux file is active');
|
|
return;
|
|
}
|
|
|
|
// Save the file first
|
|
await editor.document.save();
|
|
|
|
const filePath = editor.document.uri.fsPath;
|
|
const luxBinary = getLuxBinary();
|
|
|
|
// Create terminal and run
|
|
const terminal = vscode.window.createTerminal('Lux Run');
|
|
terminal.show();
|
|
terminal.sendText(`${luxBinary} "${filePath}"`);
|
|
}
|
|
|
|
async function formatDocument() {
|
|
const editor = vscode.window.activeTextEditor;
|
|
if (!editor || editor.document.languageId !== 'lux') {
|
|
vscode.window.showErrorMessage('No Lux file is active');
|
|
return;
|
|
}
|
|
|
|
await editor.document.save();
|
|
const formatted = await formatDocumentAsync(editor.document);
|
|
|
|
if (formatted.length > 0) {
|
|
const edit = new vscode.WorkspaceEdit();
|
|
edit.set(editor.document.uri, formatted);
|
|
await vscode.workspace.applyEdit(edit);
|
|
}
|
|
}
|
|
|
|
async function formatDocumentAsync(document: vscode.TextDocument): Promise<vscode.TextEdit[]> {
|
|
const luxBinary = getLuxBinary();
|
|
const filePath = document.uri.fsPath;
|
|
|
|
return new Promise((resolve) => {
|
|
const process = spawn(luxBinary, ['fmt', filePath]);
|
|
let stdout = '';
|
|
let stderr = '';
|
|
|
|
process.stdout.on('data', (data) => { stdout += data; });
|
|
process.stderr.on('data', (data) => { stderr += data; });
|
|
|
|
process.on('close', (code) => {
|
|
if (code === 0) {
|
|
// File was formatted, need to reload
|
|
resolve([]);
|
|
} else {
|
|
vscode.window.showErrorMessage(`Format error: ${stderr}`);
|
|
resolve([]);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function checkFile() {
|
|
const editor = vscode.window.activeTextEditor;
|
|
if (!editor || editor.document.languageId !== 'lux') {
|
|
vscode.window.showErrorMessage('No Lux file is active');
|
|
return;
|
|
}
|
|
|
|
await editor.document.save();
|
|
const filePath = editor.document.uri.fsPath;
|
|
const luxBinary = getLuxBinary();
|
|
|
|
const process = spawn(luxBinary, ['check', filePath]);
|
|
let stderr = '';
|
|
|
|
process.stderr.on('data', (data) => { stderr += data; });
|
|
|
|
process.on('close', (code) => {
|
|
if (code === 0) {
|
|
vscode.window.showInformationMessage('No type errors found');
|
|
} else {
|
|
vscode.window.showErrorMessage(`Type check failed:\n${stderr}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
function startRepl() {
|
|
if (replTerminal) {
|
|
replTerminal.dispose();
|
|
}
|
|
|
|
const luxBinary = getLuxBinary();
|
|
replTerminal = vscode.window.createTerminal({
|
|
name: 'Lux REPL',
|
|
shellPath: luxBinary,
|
|
});
|
|
replTerminal.show();
|
|
}
|
|
|
|
async function sendToRepl() {
|
|
const editor = vscode.window.activeTextEditor;
|
|
if (!editor) {
|
|
vscode.window.showErrorMessage('No active editor');
|
|
return;
|
|
}
|
|
|
|
// Get selection or current line
|
|
let text: string;
|
|
if (editor.selection.isEmpty) {
|
|
text = editor.document.lineAt(editor.selection.active.line).text;
|
|
} else {
|
|
text = editor.document.getText(editor.selection);
|
|
}
|
|
|
|
// Start REPL if not running
|
|
if (!replTerminal) {
|
|
startRepl();
|
|
// Wait a bit for REPL to start
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
}
|
|
|
|
if (replTerminal) {
|
|
replTerminal.sendText(text);
|
|
replTerminal.show();
|
|
}
|
|
}
|