Загрузка данных


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 };