Загрузка данных
const vscode = require('vscode');
const fs = require('fs');
const path = require('path');
const cp = require('child_process');
const diagnosticCollectionName = 'pawn-amxx';
let diagnostics;
function config() { return vscode.workspace.getConfiguration('pawn'); }
function isPawn(doc) { return doc && ['.sma', '.inc', '.pwn'].includes(path.extname(doc.uri.fsPath).toLowerCase()); }
function workspaceFolders() { return (vscode.workspace.workspaceFolders || []).map(f => f.uri.fsPath); }
function normalize(p) { return path.resolve(p.replace(/^~/, process.env.HOME || '~')); }
function includeDirs(doc) {
const dirs = [];
const cfg = config();
for (const p of cfg.get('includePaths', [])) dirs.push(normalize(p));
const roots = workspaceFolders();
for (const root of roots) {
dirs.push(root, path.join(root, 'include'), path.join(root, 'addons', 'amxmodx', 'scripting', 'include'));
}
const env = process.env.AMXX_INCLUDE;
if (env) dirs.push(...env.split(path.delimiter));
const compiler = findCompiler(false);
if (compiler) dirs.push(path.join(path.dirname(compiler), 'include'));
if (doc && doc.uri && doc.uri.scheme === 'file') dirs.push(path.dirname(doc.uri.fsPath));
return [...new Set(dirs.map(normalize))];
}
function findInclude(name, doc) {
const clean = name.replace(/[<>"]/g, '').trim();
const candidates = [];
if (doc && doc.uri.scheme === 'file') candidates.push(path.join(path.dirname(doc.uri.fsPath), clean));
for (const dir of includeDirs(doc)) candidates.push(path.join(dir, clean));
return candidates.find(p => { try { return fs.statSync(p).isFile(); } catch (_) { return false; } });
}
function findCompiler(showWarning = false) {
const configured = config().get('compilerPath', '').trim();
const candidates = [];
if (configured) candidates.push(normalize(configured));
candidates.push('amxxpc', 'amxxpc.exe');
for (const root of workspaceFolders()) {
candidates.push(path.join(root, 'amxxpc'), path.join(root, 'amxxpc.exe'));
candidates.push(path.join(root, 'addons', 'amxmodx', 'scripting', 'amxxpc'));
candidates.push(path.join(root, 'addons', 'amxmodx', 'scripting', 'amxxpc.exe'));
}
for (const candidate of candidates) {
if (candidate === 'amxxpc' || candidate === 'amxxpc.exe') return candidate;
try { if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate; } catch (_) {}
}
if (showWarning) vscode.window.showWarningMessage('AMX Mod X compiler not found. Set pawn.compilerPath to your amxxpc executable.');
return '';
}
function maskCommentsAndStrings(text) {
return text.replace(/\/\*[\s\S]*?\*\//g, m => m.replace(/[^\n]/g, ' '))
.replace(/\/\/.*$/gm, m => m.replace(/[^\n]/g, ' '))
.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, m => m.replace(/[^\n]/g, ' '));
}
function lineAt(text, offset) { return text.slice(0, offset).split('\n').length - 1; }
function updateDiagnostics(doc) {
if (!isPawn(doc) || !config().get('enableBasicDiagnostics', true)) return;
const text = doc.getText(), masked = maskCommentsAndStrings(text);
const diags = [];
const stack = [];
const pairs = { ')':'(', ']':'[', '}':'{' };
const openers = new Set(['(', '[', '{']);
for (let i=0; i<masked.length; i++) {
const c = masked[i];
if (openers.has(c)) stack.push({c, i});
else if (pairs[c]) {
const top = stack[stack.length-1];
if (!top || top.c !== pairs[c]) {
const start = doc.positionAt(i);
const d = new vscode.Diagnostic(new vscode.Range(start, start.translate(0,1)), `Unexpected '${c}'`, vscode.DiagnosticSeverity.Error);
d.source = 'Pawn'; diags.push(d);
} else stack.pop();
}
}
for (const item of stack) {
const pos = doc.positionAt(item.i);
const d = new vscode.Diagnostic(new vscode.Range(pos, pos.translate(0,1)), `Unclosed '${item.c}'`, vscode.DiagnosticSeverity.Error);
d.source = 'Pawn'; diags.push(d);
}
if (config().get('checkIncludes', true)) {
const re = /^\s*#\s*(?:include|tryinclude)\s*[<"]([^>"]+)[>"]/gm;
let m;
while ((m = re.exec(text))) {
if (!findInclude(m[1], doc) && /^\s*#\s*include/m.test(m[0])) {
const start = doc.positionAt(m.index + m[0].indexOf(m[1]));
const d = new vscode.Diagnostic(new vscode.Range(start, start.translate(0,m[1].length)), `Include '${m[1]}' was not found. Configure pawn.includePaths.`, vscode.DiagnosticSeverity.Warning);
d.source = 'Pawn'; diags.push(d);
}
}
}
diagnostics.set(doc.uri, diags);
}
function collectSymbols(text) {
const symbols = [];
const re = /^\s*(?:(?:public|stock|static|forward|native)\s+)*(?:[A-Za-z_][\w:]*\s+)?([A-Za-z_]\w*)\s*\(([^;\n{}]*)\)\s*(?:;|\{)?/gm;
let m;
while ((m = re.exec(text))) {
const name = m[1];
if (['if','for','while','switch'].includes(name)) continue;
symbols.push({name, args: m[2].trim(), offset: m.index + m[0].indexOf(name), line: text.slice(0,m.index).split('\n').length-1, text: m[0].trim()});
}
const vars = /^\s*(?:(?:new|static|const|stock)\s+)+(?:(?:[A-Za-z_]\w*:)?)([A-Za-z_]\w*)\s*(?:\[|=|;|,)/gm;
while ((m = vars.exec(text))) symbols.push({name:m[1], args:'', offset:m.index+m[0].indexOf(m[1]), line:text.slice(0,m.index).split('\n').length-1, text:m[0].trim(), variable:true});
return symbols;
}
function allOpenDocuments() { return vscode.workspace.textDocuments.filter(isPawn); }
async function workspacePawnFiles() {
const uris = await vscode.workspace.findFiles('**/*.{sma,inc,pwn}', '**/{.git,node_modules,bin,obj}/**', 3000);
return [...new Set([...uris.map(u=>u.fsPath), ...allOpenDocuments().map(d=>d.uri.fsPath)])];
}
async function loadSymbols() {
const out = [];
const seen = new Set();
for (const doc of allOpenDocuments()) {
for (const s of collectSymbols(doc.getText())) { if (!seen.has(s.name)) { seen.add(s.name); out.push({...s, uri:doc.uri, openDoc:true}); } }
}
for (const file of await workspacePawnFiles()) {
if (allOpenDocuments().some(d => d.uri.fsPath === file)) continue;
let text;
try { text = fs.readFileSync(file, 'utf8'); } catch (_) { continue; }
for (const s of collectSymbols(text)) { if (!seen.has(s.name)) { seen.add(s.name); out.push({...s, uri:vscode.Uri.file(file), openDoc:false}); } }
}
return out;
}
function activate(context) {
diagnostics = vscode.languages.createDiagnosticCollection(diagnosticCollectionName);
context.subscriptions.push(diagnostics);
for (const doc of vscode.workspace.textDocuments) updateDiagnostics(doc);
context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(updateDiagnostics));
context.subscriptions.push(vscode.workspace.onDidChangeTextDocument(e => updateDiagnostics(e.document)));
context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(updateDiagnostics));
context.subscriptions.push(vscode.languages.registerCompletionItemProvider({language:'pawn'}, {
async provideCompletionItems(document, position) {
const line = document.lineAt(position.line).text.slice(0, position.character);
const includeMatch = line.match(/#\s*(?:include|tryinclude)\s*[<"]([^>"]*)$/);
if (includeMatch) {
const items = [];
const prefix = includeMatch[1];
for (const dir of includeDirs(document)) {
let names = [];
try { names = fs.readdirSync(dir); } catch (_) {}
for (const name of names.filter(n => n.toLowerCase().endsWith('.inc') && n.startsWith(path.basename(prefix)))) {
const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.File);
item.detail = dir; items.push(item);
}
}
return items;
}
const symbols = await loadSymbols();
const items = [];
const seen = new Set();
for (const s of symbols) {
if (seen.has(s.name)) continue; seen.add(s.name);
const item = new vscode.CompletionItem(s.name, s.variable ? vscode.CompletionItemKind.Variable : vscode.CompletionItemKind.Function);
item.detail = s.openDoc ? `Workspace symbol · ${s.text}` : `Workspace symbol · ${path.basename(s.uri.fsPath)}`;
item.documentation = new vscode.MarkdownString('```pawn\n' + s.text + '\n```');
items.push(item);
}
const builtins = [
['register_plugin','register_plugin(const plugin_name[], const version[], const author[])'],
['register_event','register_event(const event[], const function[], const flags[], const cond[] = "", ... )'],
['register_clcmd','register_clcmd(const client_command[], const function[], flags = -1, const info[] = "", ... )'],
['client_print','client_print(index, type, const message[], ...)'],
['server_print','server_print(const message[], ...)'],
['is_user_connected','is_user_connected(index)'],
['is_user_alive','is_user_alive(index)'],
['get_user_name','get_user_name(index, name[], len)'],
['get_user_authid','get_user_authid(index, authid[], len)'],
['get_players','get_players(players[], &num, flags[] = "", const team[] = "")'],
['set_task','set_task(Float:time, const function[], id = 0, parameter[] = "", len = 0, flags[] = "", repeat = 0)'],
['remove_task','remove_task(id = 0, outside = 0)'],
['ExecuteHamB','ExecuteHamB(Ham:function, this, ...)'],
['PLUGIN_CONTINUE','PLUGIN_CONTINUE'], ['PLUGIN_HANDLED','PLUGIN_HANDLED'],
['PLUGIN_STOP','PLUGIN_STOP'], ['MAX_PLAYERS','MAX_PLAYERS']
];
for (const [name, signature] of builtins) {
const item = new vscode.CompletionItem(name, name === signature ? vscode.CompletionItemKind.Constant : vscode.CompletionItemKind.Function);
item.detail = 'AMX Mod X'; item.documentation = new vscode.MarkdownString('```pawn\n'+signature+'\n```');
items.push(item);
}
return items;
}
}, '.', '#', '"', '<'));
context.subscriptions.push(vscode.languages.registerHoverProvider({language:'pawn'}, {
async provideHover(document, position) {
const word = document.getWordRangeAtPosition(position, /[A-Za-z_]\w*/);
if (!word) return;
const name = document.getText(word);
const text = document.getText();
const symbols = collectSymbols(text).filter(s => s.name === name);
if (symbols.length) return new vscode.Hover(new vscode.MarkdownString('```pawn\n'+symbols[0].text+'\n```\n\n*Defined in this file*'));
const all = await loadSymbols();
const s = all.find(x => x.name === name);
if (s) return new vscode.Hover(new vscode.MarkdownString('```pawn\n'+s.text+'\n```\n\n*'+path.basename(s.uri.fsPath)+'*'));
const builtin = {
register_plugin:'register_plugin(const plugin_name[], const version[], const author[]) — set plugin metadata.',
register_event:'register_event(const event[], const function[], const flags[], ...) — register a game event callback.',
register_clcmd:'register_clcmd(const client_command[], const function[], ...) — register a client command.',
client_print:'client_print(index, print_type, const message[], ...) — print text to a client.',
is_user_alive:'is_user_alive(index) — test whether a player is alive.',
is_user_connected:'is_user_connected(index) — test whether a player is connected.',
PLUGIN_CONTINUE:'Return value: allow further plugin handling.',
PLUGIN_HANDLED:'Return value: mark the command/event as handled.'
};
if (builtin[name]) return new vscode.Hover(new vscode.MarkdownString('```pawn\n'+builtin[name]+'\n```'));
}
}));
context.subscriptions.push(vscode.languages.registerDefinitionProvider({language:'pawn'}, {
async provideDefinition(document, position) {
const range = document.getWordRangeAtPosition(position, /[A-Za-z_]\w*/);
if (!range) return;
const name = document.getText(range);
const local = collectSymbols(document.getText()).find(s => s.name === name);
if (local) return new vscode.Location(document.uri, document.positionAt(local.offset));
const symbols = await loadSymbols();
const found = symbols.find(s => s.name === name);
if (found) return new vscode.Location(found.uri, new vscode.Position(found.line, Math.max(0, found.offset - (found.text || '').length + (found.text || '').indexOf(name))));
const includeLine = document.lineAt(position.line).text.match(/#\s*include\s*[<"]([^>"]+)[>"]/);
if (includeLine) {
const file = findInclude(includeLine[1], document);
if (file) return new vscode.Location(vscode.Uri.file(file), new vscode.Position(0,0));
}
}
}));
context.subscriptions.push(vscode.commands.registerCommand('pawn.compileCurrentFile', () => compileCurrentFile()));
context.subscriptions.push(vscode.commands.registerCommand('pawn.setCompilerPath', async () => {
const value = await vscode.window.showInputBox({prompt:'Absolute path to amxxpc (or amxxpc.exe)', value:config().get('compilerPath',''), placeHolder:'/path/to/amxxpc'});
if (value !== undefined) await config().update('compilerPath', value, vscode.ConfigurationTarget.Workspace);
}));
context.subscriptions.push(vscode.commands.registerCommand('pawn.refreshIncludes', async () => {
for (const doc of allOpenDocuments()) updateDiagnostics(doc);
vscode.window.showInformationMessage('Pawn include paths refreshed.');
}));
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('pawn')) for (const doc of allOpenDocuments()) updateDiagnostics(doc);
}));
}
function compileCurrentFile() {
const editor = vscode.window.activeTextEditor;
if (!editor || !isPawn(editor.document)) {
vscode.window.showErrorMessage('Open a .sma, .inc or .pwn file to compile.');
return;
}
const source = editor.document;
if (path.extname(source.uri.fsPath).toLowerCase() === '.inc') {
vscode.window.showWarningMessage('Include files (.inc) are not usually compiled directly. Open the main .sma file.');
return;
}
source.save().then(() => {
const compiler = findCompiler(true);
if (!compiler) return;
const output = vscode.window.createOutputChannel('AMX Mod X Compiler');
output.show(true);
const args = [...config().get('compilerArgs', []), source.uri.fsPath];
const cwd = path.dirname(source.uri.fsPath);
const proc = cp.spawn(compiler, args, {cwd, windowsHide:true});
proc.stdout.on('data', d => output.append(d.toString()));
proc.stderr.on('data', d => output.append(d.toString()));
proc.on('error', e => output.appendLine(`Failed to start compiler: ${e.message}`));
proc.on('close', code => {
output.appendLine(`\nCompiler exited with code ${code}.`);
if (code === 0) vscode.window.showInformationMessage('AMX Mod X compilation finished.');
else vscode.window.showErrorMessage(`AMX Mod X compilation failed (exit code ${code}). See Output → AMX Mod X Compiler.`);
});
});
}
function deactivate() { if (diagnostics) diagnostics.dispose(); }
module.exports = { activate, deactivate };