リポジトリ / emssr / independent_common_compiler
r2
| r2 emssr | 1 | // Common scope の VS Code 拡張。 |
| r2 emssr | 2 | // |
| r2 emssr | 3 | // web 版と同じ server.py を使う。拡張が足すのは三つだけになる。 |
| r2 emssr | 4 | // |
| r2 emssr | 5 | // 1. server.py の起動と停止 |
| r2 emssr | 6 | // 2. notebook を webview で開く |
| r2 emssr | 7 | // 3. 編集中の .cmn を hub へ送り、開いている全パネルを更新する |
| r2 emssr | 8 | // |
| r2 emssr | 9 | // 計算は拡張の中に無い。平文は server の /state へ渡り、各ウィンドウの |
| r2 emssr | 10 | // kernel がそれぞれ実行する。lane の作業領域は交わらない(20 式 1.2)ので、 |
| r2 emssr | 11 | // 何枚開いても state が混ざらない。 |
| r2 emssr | 12 | |
| r2 emssr | 13 | const vscode = require('vscode'); |
| r2 emssr | 14 | const cp = require('child_process'); |
| r2 emssr | 15 | const fs = require('fs'); |
| r2 emssr | 16 | const path = require('path'); |
| r2 emssr | 17 | const http = require('http'); |
| r2 emssr | 18 | |
| r2 emssr | 19 | let server = null; // 起動した server.py の process |
| r2 emssr | 20 | let out = null; // その標準出力を写す channel |
| r2 emssr | 21 | |
| r2 emssr | 22 | function cfg(key) { |
| r2 emssr | 23 | return vscode.workspace.getConfiguration('commonScope').get(key); |
| r2 emssr | 24 | } |
| r2 emssr | 25 | |
| r2 emssr | 26 | // ===== server.py の場所 ===== |
| r2 emssr | 27 | // |
| r2 emssr | 28 | // 設定が空なら、開いている folder の下から common_scope_notebook/server.py を探す。 |
| r2 emssr | 29 | |
| r2 emssr | 30 | function findRoot() { |
| r2 emssr | 31 | const set = cfg('root'); |
| r2 emssr | 32 | if (set) return set; |
| r2 emssr | 33 | const rel = path.join('continuous_republic', 'common_apps', 'common_scope_notebook'); |
| r2 emssr | 34 | for (const f of vscode.workspace.workspaceFolders || []) { |
| r2 emssr | 35 | for (const c of [path.join(f.uri.fsPath, rel), f.uri.fsPath]) { |
| r2 emssr | 36 | if (fs.existsSync(path.join(c, 'server.py'))) return c; |
| r2 emssr | 37 | } |
| r2 emssr | 38 | } |
| r2 emssr | 39 | return null; |
| r2 emssr | 40 | } |
| r2 emssr | 41 | |
| r2 emssr | 42 | function url(pathname) { |
| r2 emssr | 43 | return `http://127.0.0.1:${cfg('port')}${pathname || '/'}`; |
| r2 emssr | 44 | } |
| r2 emssr | 45 | |
| r2 emssr | 46 | function alive() { |
| r2 emssr | 47 | return new Promise(ok => { |
| r2 emssr | 48 | const req = http.get(url('/api/state'), r => { r.resume(); ok(r.statusCode === 200); }); |
| r2 emssr | 49 | req.on('error', () => ok(false)); |
| r2 emssr | 50 | req.setTimeout(400, () => { req.destroy(); ok(false); }); |
| r2 emssr | 51 | }); |
| r2 emssr | 52 | } |
| r2 emssr | 53 | |
| r2 emssr | 54 | async function ensureServer() { |
| r2 emssr | 55 | if (await alive()) return true; |
| r2 emssr | 56 | const root = findRoot(); |
| r2 emssr | 57 | if (!root) { |
| r2 emssr | 58 | vscode.window.showErrorMessage( |
| r2 emssr | 59 | 'common_scope_notebook が見つかりません。設定 commonScope.root にフォルダを書いてください。'); |
| r2 emssr | 60 | return false; |
| r2 emssr | 61 | } |
| r2 emssr | 62 | out = out || vscode.window.createOutputChannel('Common scope'); |
| r2 emssr | 63 | server = cp.spawn(cfg('python'), |
| r2 emssr | 64 | [path.join(root, 'server.py'), '--port', String(cfg('port')), '--no-browser'], |
| r2 emssr | 65 | { cwd: root }); |
| r2 emssr | 66 | server.stdout.on('data', d => out.append(d.toString())); |
| r2 emssr | 67 | server.stderr.on('data', d => out.append(d.toString())); |
| r2 emssr | 68 | server.on('exit', c => { out.appendLine(`server 終了 (${c})`); server = null; }); |
| r2 emssr | 69 | |
| r2 emssr | 70 | for (let k = 0; k < 40; k++) { |
| r2 emssr | 71 | await new Promise(r => setTimeout(r, 250)); |
| r2 emssr | 72 | if (await alive()) return true; |
| r2 emssr | 73 | } |
| r2 emssr | 74 | out.show(true); |
| r2 emssr | 75 | vscode.window.showErrorMessage('server.py が起動しません。出力を確認してください。'); |
| r2 emssr | 76 | return false; |
| r2 emssr | 77 | } |
| r2 emssr | 78 | |
| r2 emssr | 79 | // ===== hub へ送る ===== |
| r2 emssr | 80 | |
| r2 emssr | 81 | function post(body) { |
| r2 emssr | 82 | return new Promise((ok, no) => { |
| r2 emssr | 83 | const data = Buffer.from(JSON.stringify(body), 'utf8'); |
| r2 emssr | 84 | const req = http.request(url('/state'), |
| r2 emssr | 85 | { method: 'POST', headers: { 'Content-Length': data.length } }, |
| r2 emssr | 86 | r => { r.resume(); ok(r.statusCode); }); |
| r2 emssr | 87 | req.on('error', no); |
| r2 emssr | 88 | req.end(data); |
| r2 emssr | 89 | }); |
| r2 emssr | 90 | } |
| r2 emssr | 91 | |
| r2 emssr | 92 | // 同じフォルダに .dic があれば採用語彙として一緒に送る |
| r2 emssr | 93 | function dicBeside(file) { |
| r2 emssr | 94 | const dir = path.dirname(file); |
| r2 emssr | 95 | const own = path.join(dir, path.basename(file, '.cmn') + '.dic'); |
| r2 emssr | 96 | if (fs.existsSync(own)) return fs.readFileSync(own, 'utf8'); |
| r2 emssr | 97 | for (const f of fs.readdirSync(dir)) { |
| r2 emssr | 98 | if (f.endsWith('.dic')) return fs.readFileSync(path.join(dir, f), 'utf8'); |
| r2 emssr | 99 | } |
| r2 emssr | 100 | return null; |
| r2 emssr | 101 | } |
| r2 emssr | 102 | |
| r2 emssr | 103 | async function send() { |
| r2 emssr | 104 | const ed = vscode.window.activeTextEditor; |
| r2 emssr | 105 | if (!ed || !ed.document.fileName.endsWith('.cmn')) { |
| r2 emssr | 106 | vscode.window.showWarningMessage('.cmn を開いてから実行してください。'); |
| r2 emssr | 107 | return; |
| r2 emssr | 108 | } |
| r2 emssr | 109 | if (!(await ensureServer())) return; |
| r2 emssr | 110 | await post({ |
| r2 emssr | 111 | notebook: path.basename(ed.document.fileName), |
| r2 emssr | 112 | spec: { source: ed.document.getText(), dic: dicBeside(ed.document.fileName), |
| r2 emssr | 113 | seeds: null, words: null, budget: null }, |
| r2 emssr | 114 | view: { inputs: null, cut: null, out: 0 }, |
| r2 emssr | 115 | }); |
| r2 emssr | 116 | } |
| r2 emssr | 117 | |
| r2 emssr | 118 | // ===== webview ===== |
| r2 emssr | 119 | |
| r2 emssr | 120 | function panel(title, pathname) { |
| r2 emssr | 121 | const p = vscode.window.createWebviewPanel('commonScope', title, |
| r2 emssr | 122 | vscode.ViewColumn.Beside, { enableScripts: true, retainContextWhenHidden: true }); |
| r2 emssr | 123 | const src = url(pathname); |
| r2 emssr | 124 | p.webview.html = `<!DOCTYPE html><html><head><meta charset="utf-8"> |
| r2 emssr | 125 | <style>html,body,iframe{margin:0;padding:0;height:100%;width:100%;border:0}</style> |
| r2 emssr | 126 | </head><body><iframe src="${src}"></iframe></body></html>`; |
| r2 emssr | 127 | return p; |
| r2 emssr | 128 | } |
| r2 emssr | 129 | |
| r2 emssr | 130 | const PANES = { |
| r2 emssr | 131 | '層と机': 'chamber', '箱の DAG': 'dag', 'ツイ数平面': 'tsui', |
| r2 emssr | 132 | 'lane64': 'lane', '接合秩': 'joint', '音節と箱': 'text', |
| r2 emssr | 133 | }; |
| r2 emssr | 134 | |
| r2 emssr | 135 | function activate(ctx) { |
| r2 emssr | 136 | ctx.subscriptions.push( |
| r2 emssr | 137 | vscode.commands.registerCommand('commonScope.open', async () => { |
| r2 emssr | 138 | if (await ensureServer()) panel('Common scope notebook', '/'); |
| r2 emssr | 139 | }), |
| r2 emssr | 140 | vscode.commands.registerCommand('commonScope.send', send), |
| r2 emssr | 141 | vscode.commands.registerCommand('commonScope.pane', async () => { |
| r2 emssr | 142 | const pick = await vscode.window.showQuickPick(Object.keys(PANES), |
| r2 emssr | 143 | { placeHolder: '開くパネル' }); |
| r2 emssr | 144 | if (!pick) return; |
| r2 emssr | 145 | if (await ensureServer()) panel(pick, '/?pane=' + PANES[pick]); |
| r2 emssr | 146 | }), |
| r2 emssr | 147 | vscode.commands.registerCommand('commonScope.stop', () => { |
| r2 emssr | 148 | if (server) { server.kill(); server = null; } |
| r2 emssr | 149 | })); |
| r2 emssr | 150 | } |
| r2 emssr | 151 | |
| r2 emssr | 152 | function deactivate() { |
| r2 emssr | 153 | if (server) server.kill(); |
| r2 emssr | 154 | } |
| r2 emssr | 155 | |
| r2 emssr | 156 | module.exports = { activate, deactivate }; |