@void2610/tyranoscript-lsp 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +26 -0
  2. package/dist/server.js +355 -20
  3. package/package.json +4 -2
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # @void2610/tyranoscript-lsp
2
+
3
+ [TyranoScript](https://tyrano.jp/)向けLanguage Serverです。[Zed](https://zed.dev/)拡張 [tyranoscript-zed](https://github.com/void2610/tyranoscript-zed) から利用されます。
4
+
5
+ ## 機能
6
+
7
+ - タグ名の補完(`[` または `@` 入力時)
8
+ - パラメータの補完(使用済みパラメータを自動除外)
9
+ - 必須パラメータのスニペット自動挿入
10
+ - ホバーによるタグ・パラメータのドキュメント表示
11
+
12
+ ## 開発
13
+
14
+ ```bash
15
+ npm install
16
+ npm run build # dist/server.js にバンドル
17
+ npm run watch # ファイル変更時に自動ビルド
18
+ ```
19
+
20
+ ## 謝辞
21
+
22
+ タグ辞書データは [orukRed/tyranosyntax](https://github.com/orukRed/tyranosyntax)(VSCode拡張)の `tyrano.Tooltip.json` を基に作成しました。
23
+
24
+ ## ライセンス
25
+
26
+ [MIT](../LICENSE)
package/dist/server.js CHANGED
@@ -3081,7 +3081,7 @@ var require_main = __commonJS({
3081
3081
  exports2.createMessageConnection = exports2.createServerSocketTransport = exports2.createClientSocketTransport = exports2.createServerPipeTransport = exports2.createClientPipeTransport = exports2.generateRandomPipeName = exports2.StreamMessageWriter = exports2.StreamMessageReader = exports2.SocketMessageWriter = exports2.SocketMessageReader = exports2.PortMessageWriter = exports2.PortMessageReader = exports2.IPCMessageWriter = exports2.IPCMessageReader = void 0;
3082
3082
  var ril_1 = require_ril();
3083
3083
  ril_1.default.install();
3084
- var path = require("path");
3084
+ var path2 = require("path");
3085
3085
  var os = require("os");
3086
3086
  var crypto_1 = require("crypto");
3087
3087
  var net_1 = require("net");
@@ -3217,9 +3217,9 @@ var require_main = __commonJS({
3217
3217
  }
3218
3218
  let result;
3219
3219
  if (XDG_RUNTIME_DIR) {
3220
- result = path.join(XDG_RUNTIME_DIR, `vscode-ipc-${randomSuffix}.sock`);
3220
+ result = path2.join(XDG_RUNTIME_DIR, `vscode-ipc-${randomSuffix}.sock`);
3221
3221
  } else {
3222
- result = path.join(os.tmpdir(), `vscode-${randomSuffix}.sock`);
3222
+ result = path2.join(os.tmpdir(), `vscode-${randomSuffix}.sock`);
3223
3223
  }
3224
3224
  const limit = safeIpcPathLengths.get(process.platform);
3225
3225
  if (limit !== void 0 && result.length > limit) {
@@ -8309,8 +8309,8 @@ var require_files = __commonJS({
8309
8309
  Object.defineProperty(exports2, "__esModule", { value: true });
8310
8310
  exports2.resolveModulePath = exports2.FileSystem = exports2.resolveGlobalYarnPath = exports2.resolveGlobalNodePath = exports2.resolve = exports2.uriToFilePath = void 0;
8311
8311
  var url = require("url");
8312
- var path = require("path");
8313
- var fs = require("fs");
8312
+ var path2 = require("path");
8313
+ var fs2 = require("fs");
8314
8314
  var child_process_1 = require("child_process");
8315
8315
  function uriToFilePath(uri) {
8316
8316
  let parsed = url.parse(uri);
@@ -8328,7 +8328,7 @@ var require_files = __commonJS({
8328
8328
  segments.shift();
8329
8329
  }
8330
8330
  }
8331
- return path.normalize(segments.join("/"));
8331
+ return path2.normalize(segments.join("/"));
8332
8332
  }
8333
8333
  exports2.uriToFilePath = uriToFilePath;
8334
8334
  function isWindows() {
@@ -8357,9 +8357,9 @@ var require_files = __commonJS({
8357
8357
  let env = process.env;
8358
8358
  let newEnv = /* @__PURE__ */ Object.create(null);
8359
8359
  Object.keys(env).forEach((key) => newEnv[key] = env[key]);
8360
- if (nodePath && fs.existsSync(nodePath)) {
8360
+ if (nodePath && fs2.existsSync(nodePath)) {
8361
8361
  if (newEnv[nodePathKey]) {
8362
- newEnv[nodePathKey] = nodePath + path.delimiter + newEnv[nodePathKey];
8362
+ newEnv[nodePathKey] = nodePath + path2.delimiter + newEnv[nodePathKey];
8363
8363
  } else {
8364
8364
  newEnv[nodePathKey] = nodePath;
8365
8365
  }
@@ -8432,9 +8432,9 @@ var require_files = __commonJS({
8432
8432
  }
8433
8433
  if (prefix.length > 0) {
8434
8434
  if (isWindows()) {
8435
- return path.join(prefix, "node_modules");
8435
+ return path2.join(prefix, "node_modules");
8436
8436
  } else {
8437
- return path.join(prefix, "lib", "node_modules");
8437
+ return path2.join(prefix, "lib", "node_modules");
8438
8438
  }
8439
8439
  }
8440
8440
  return void 0;
@@ -8474,7 +8474,7 @@ var require_files = __commonJS({
8474
8474
  try {
8475
8475
  let yarn = JSON.parse(line);
8476
8476
  if (yarn.type === "log") {
8477
- return path.join(yarn.data, "node_modules");
8477
+ return path2.join(yarn.data, "node_modules");
8478
8478
  }
8479
8479
  } catch (e) {
8480
8480
  }
@@ -8497,24 +8497,24 @@ var require_files = __commonJS({
8497
8497
  if (process.platform === "win32") {
8498
8498
  _isCaseSensitive = false;
8499
8499
  } else {
8500
- _isCaseSensitive = !fs.existsSync(__filename.toUpperCase()) || !fs.existsSync(__filename.toLowerCase());
8500
+ _isCaseSensitive = !fs2.existsSync(__filename.toUpperCase()) || !fs2.existsSync(__filename.toLowerCase());
8501
8501
  }
8502
8502
  return _isCaseSensitive;
8503
8503
  }
8504
8504
  FileSystem2.isCaseSensitive = isCaseSensitive;
8505
8505
  function isParent(parent, child) {
8506
8506
  if (isCaseSensitive()) {
8507
- return path.normalize(child).indexOf(path.normalize(parent)) === 0;
8507
+ return path2.normalize(child).indexOf(path2.normalize(parent)) === 0;
8508
8508
  } else {
8509
- return path.normalize(child).toLowerCase().indexOf(path.normalize(parent).toLowerCase()) === 0;
8509
+ return path2.normalize(child).toLowerCase().indexOf(path2.normalize(parent).toLowerCase()) === 0;
8510
8510
  }
8511
8511
  }
8512
8512
  FileSystem2.isParent = isParent;
8513
8513
  })(FileSystem || (exports2.FileSystem = FileSystem = {}));
8514
8514
  function resolveModulePath(workspaceRoot, moduleName, nodePath, tracer) {
8515
8515
  if (nodePath) {
8516
- if (!path.isAbsolute(nodePath)) {
8517
- nodePath = path.join(workspaceRoot, nodePath);
8516
+ if (!path2.isAbsolute(nodePath)) {
8517
+ nodePath = path2.join(workspaceRoot, nodePath);
8518
8518
  }
8519
8519
  return resolve(moduleName, nodePath, nodePath, tracer).then((value) => {
8520
8520
  if (FileSystem.isParent(nodePath, value)) {
@@ -12057,16 +12057,266 @@ vmax\u5C5E\u6027\u30920\u306B\u8A2D\u5B9A\u3059\u308B\u3068\u6A2A\u63FA\u308C\u3
12057
12057
  ]);
12058
12058
  var TAG_NAMES = Array.from(TAG_DATABASE.keys());
12059
12059
 
12060
+ // src/workspaceScanner.ts
12061
+ var fs = __toESM(require("fs"));
12062
+ var path = __toESM(require("path"));
12063
+ var TAG_STORAGE_MAPPING = /* @__PURE__ */ new Map([
12064
+ ["bg", "bgimage"],
12065
+ ["bg2", "bgimage"],
12066
+ ["chara_new", "fgimage"],
12067
+ ["chara_face", "fgimage"],
12068
+ ["chara_mod", "fgimage"],
12069
+ ["chara_show", "fgimage"],
12070
+ ["chara_layer", "fgimage"],
12071
+ ["image", "image"],
12072
+ ["cursor", "image"],
12073
+ ["graph", "image"],
12074
+ ["mask", "image"],
12075
+ ["playbgm", "bgm"],
12076
+ ["fadeinbgm", "bgm"],
12077
+ ["xchgbgm", "bgm"],
12078
+ ["playse", "sound"],
12079
+ ["fadeinse", "sound"],
12080
+ ["movie", "video"],
12081
+ ["bgmovie", "video"],
12082
+ ["layer_video", "video"],
12083
+ ["jump", "scenario"],
12084
+ ["call", "scenario"],
12085
+ ["link", "scenario"],
12086
+ ["glink", "scenario"],
12087
+ ["clickable", "scenario"],
12088
+ ["button", "scenario"]
12089
+ ]);
12090
+ var CACHE_TTL = 3e4;
12091
+ var WorkspaceScanner = class {
12092
+ constructor() {
12093
+ this.rootPath = "";
12094
+ this.dataPath = "";
12095
+ this.initialized = false;
12096
+ // アセットファイルキャッシュ(カテゴリ別)
12097
+ this.assetCache = /* @__PURE__ */ new Map();
12098
+ // KSファイルインデックス(ファイルパスをキーに)
12099
+ this.ksFileIndices = /* @__PURE__ */ new Map();
12100
+ }
12101
+ /**
12102
+ * ワークスペースルートを設定しdataディレクトリの存在を確認する
12103
+ */
12104
+ initialize(rootUri) {
12105
+ try {
12106
+ const url = new URL(rootUri);
12107
+ this.rootPath = decodeURIComponent(url.pathname);
12108
+ this.dataPath = path.join(this.rootPath, "data");
12109
+ if (fs.existsSync(this.dataPath)) {
12110
+ this.initialized = true;
12111
+ return true;
12112
+ }
12113
+ } catch {
12114
+ }
12115
+ this.initialized = false;
12116
+ return false;
12117
+ }
12118
+ /**
12119
+ * アセットスキャンとKSファイルスキャンを並行実行する
12120
+ */
12121
+ async scanAll() {
12122
+ if (!this.initialized) return;
12123
+ await Promise.all([this.scanAssets(), this.scanKsFiles()]);
12124
+ }
12125
+ /**
12126
+ * 全アセットカテゴリのディレクトリを走査する
12127
+ */
12128
+ async scanAssets() {
12129
+ const categories = [
12130
+ "bgimage",
12131
+ "fgimage",
12132
+ "image",
12133
+ "bgm",
12134
+ "sound",
12135
+ "video",
12136
+ "scenario",
12137
+ "others"
12138
+ ];
12139
+ for (const category of categories) {
12140
+ this.scanAssetCategory(category);
12141
+ }
12142
+ }
12143
+ /**
12144
+ * 指定カテゴリのアセットディレクトリを走査しキャッシュに格納する
12145
+ */
12146
+ scanAssetCategory(category) {
12147
+ const dirPath = path.join(this.dataPath, category);
12148
+ try {
12149
+ if (!fs.existsSync(dirPath)) {
12150
+ this.assetCache.set(category, { files: [], timestamp: Date.now() });
12151
+ return;
12152
+ }
12153
+ const files = this.readDirRecursive(dirPath, dirPath);
12154
+ this.assetCache.set(category, { files, timestamp: Date.now() });
12155
+ } catch {
12156
+ this.assetCache.set(category, { files: [], timestamp: Date.now() });
12157
+ }
12158
+ }
12159
+ /**
12160
+ * ディレクトリを再帰的に走査しファイルの相対パスリストを返す
12161
+ */
12162
+ readDirRecursive(dirPath, basePath) {
12163
+ const results = [];
12164
+ try {
12165
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
12166
+ for (const entry of entries) {
12167
+ const fullPath = path.join(dirPath, entry.name);
12168
+ if (entry.isDirectory()) {
12169
+ results.push(...this.readDirRecursive(fullPath, basePath));
12170
+ } else {
12171
+ results.push(path.relative(basePath, fullPath));
12172
+ }
12173
+ }
12174
+ } catch {
12175
+ }
12176
+ return results;
12177
+ }
12178
+ /**
12179
+ * data/scenario/ 配下の .ks ファイルを全件読み込み、ラベルとマクロを抽出する
12180
+ */
12181
+ async scanKsFiles() {
12182
+ const scenarioPath = path.join(this.dataPath, "scenario");
12183
+ if (!fs.existsSync(scenarioPath)) return;
12184
+ const ksFiles = this.findKsFiles(scenarioPath);
12185
+ this.ksFileIndices.clear();
12186
+ for (const filePath of ksFiles) {
12187
+ try {
12188
+ const content = fs.readFileSync(filePath, "utf-8");
12189
+ const relativePath = path.relative(this.dataPath, filePath);
12190
+ this.indexKsContent(relativePath, content);
12191
+ } catch {
12192
+ }
12193
+ }
12194
+ }
12195
+ /**
12196
+ * 指定ディレクトリ配下の .ks ファイルを再帰的に検索する
12197
+ */
12198
+ findKsFiles(dirPath) {
12199
+ const results = [];
12200
+ try {
12201
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
12202
+ for (const entry of entries) {
12203
+ const fullPath = path.join(dirPath, entry.name);
12204
+ if (entry.isDirectory()) {
12205
+ results.push(...this.findKsFiles(fullPath));
12206
+ } else if (entry.name.endsWith(".ks")) {
12207
+ results.push(fullPath);
12208
+ }
12209
+ }
12210
+ } catch {
12211
+ }
12212
+ return results;
12213
+ }
12214
+ /**
12215
+ * KSファイルの内容からラベルとマクロを正規表現で抽出しインデックスに格納する
12216
+ */
12217
+ indexKsContent(relativePath, content) {
12218
+ const labels = [];
12219
+ const macros = [];
12220
+ const lines = content.split("\n");
12221
+ for (let i = 0; i < lines.length; i++) {
12222
+ const line = lines[i];
12223
+ const labelMatch = line.match(/^\*(\w+)/);
12224
+ if (labelMatch) {
12225
+ labels.push({
12226
+ name: labelMatch[1],
12227
+ file: relativePath,
12228
+ line: i
12229
+ });
12230
+ }
12231
+ const macroMatch = line.match(/\[macro\s+name\s*=\s*"(\w+)"\s*\]/i);
12232
+ if (macroMatch) {
12233
+ macros.push({
12234
+ name: macroMatch[1],
12235
+ file: relativePath,
12236
+ line: i
12237
+ });
12238
+ }
12239
+ }
12240
+ this.ksFileIndices.set(relativePath, { labels, macros });
12241
+ }
12242
+ /**
12243
+ * 単一ファイルのインクリメンタル更新(編集中ファイルのインデックスを差し替え)
12244
+ */
12245
+ updateFile(uri, content) {
12246
+ if (!this.initialized) return;
12247
+ try {
12248
+ const url = new URL(uri);
12249
+ const filePath = decodeURIComponent(url.pathname);
12250
+ const relativePath = path.relative(this.dataPath, filePath);
12251
+ if (relativePath.startsWith("scenario") && filePath.endsWith(".ks")) {
12252
+ this.indexKsContent(relativePath, content);
12253
+ }
12254
+ } catch {
12255
+ }
12256
+ }
12257
+ /**
12258
+ * 指定カテゴリのアセットファイル一覧を返す
12259
+ * キャッシュTTL超過時は自動再スキャンする
12260
+ */
12261
+ getAssetsForCategory(category) {
12262
+ if (!this.initialized) return [];
12263
+ const cached = this.assetCache.get(category);
12264
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
12265
+ return cached.files;
12266
+ }
12267
+ this.scanAssetCategory(category);
12268
+ return this.assetCache.get(category)?.files ?? [];
12269
+ }
12270
+ /**
12271
+ * 全ラベル定義を返す
12272
+ */
12273
+ getLabels() {
12274
+ const labels = [];
12275
+ for (const index of this.ksFileIndices.values()) {
12276
+ labels.push(...index.labels);
12277
+ }
12278
+ return labels;
12279
+ }
12280
+ /**
12281
+ * 全マクロ定義を返す
12282
+ */
12283
+ getMacros() {
12284
+ const macros = [];
12285
+ for (const index of this.ksFileIndices.values()) {
12286
+ macros.push(...index.macros);
12287
+ }
12288
+ return macros;
12289
+ }
12290
+ /**
12291
+ * シナリオファイル一覧を返す(.ks拡張子)
12292
+ */
12293
+ getScenarioFiles() {
12294
+ return this.getAssetsForCategory("scenario");
12295
+ }
12296
+ /**
12297
+ * 初期化済みかどうかを返す
12298
+ */
12299
+ isInitialized() {
12300
+ return this.initialized;
12301
+ }
12302
+ };
12303
+
12060
12304
  // src/server.ts
12061
12305
  var connection = (0, import_node.createConnection)(import_node.ProposedFeatures.all);
12062
12306
  var documents = new import_node.TextDocuments(TextDocument);
12063
- connection.onInitialize((_params) => {
12307
+ var scanner = new WorkspaceScanner();
12308
+ connection.onInitialize((params) => {
12309
+ const rootUri = params.rootUri ?? params.workspaceFolders?.[0]?.uri;
12310
+ if (rootUri && scanner.initialize(rootUri)) {
12311
+ scanner.scanAll().catch(() => {
12312
+ });
12313
+ }
12064
12314
  return {
12065
12315
  capabilities: {
12066
12316
  textDocumentSync: import_node.TextDocumentSyncKind.Incremental,
12067
12317
  completionProvider: {
12068
- // "[" "@" で補完をトリガー
12069
- triggerCharacters: ["[", "@", " "],
12318
+ // "[", "@", スペース, '"' で補完をトリガー
12319
+ triggerCharacters: ["[", "@", " ", '"'],
12070
12320
  resolveProvider: false
12071
12321
  },
12072
12322
  hoverProvider: true
@@ -12112,6 +12362,18 @@ function isTagNameTrigger(lineText, character) {
12112
12362
  }
12113
12363
  return null;
12114
12364
  }
12365
+ function getParamValueContext(lineText, character) {
12366
+ const tagCtx = getTagContext(lineText, character);
12367
+ if (!tagCtx) return null;
12368
+ const textUpToCursor = lineText.substring(0, character);
12369
+ const valueMatch = textUpToCursor.match(/(\w+)\s*=\s*"([^"]*)$/);
12370
+ if (!valueMatch) return null;
12371
+ return {
12372
+ tagName: tagCtx.tagName,
12373
+ paramName: valueMatch[1],
12374
+ currentValue: valueMatch[2]
12375
+ };
12376
+ }
12115
12377
  function createTagCompletions(trigger) {
12116
12378
  return TAG_NAMES.map((name, index) => {
12117
12379
  const tag = TAG_DATABASE.get(name);
@@ -12189,6 +12451,53 @@ function createTagDocumentation(tag) {
12189
12451
  }
12190
12452
  return doc;
12191
12453
  }
12454
+ function createMacroCompletions(trigger) {
12455
+ const macros = scanner.getMacros();
12456
+ return macros.map((macro, index) => {
12457
+ const insertText = trigger === "bracket" ? `${macro.name}]` : macro.name;
12458
+ return {
12459
+ label: macro.name,
12460
+ kind: import_node.CompletionItemKind.Function,
12461
+ detail: `\u30DE\u30AF\u30ED (${macro.file})`,
12462
+ documentation: {
12463
+ kind: import_node.MarkupKind.Markdown,
12464
+ value: `**[${macro.name}]** \u2014 \u30E6\u30FC\u30B6\u30FC\u5B9A\u7FA9\u30DE\u30AF\u30ED
12465
+
12466
+ \u5B9A\u7FA9\u5143: \`${macro.file}\` (\u884C ${macro.line + 1})`
12467
+ },
12468
+ insertText,
12469
+ insertTextFormat: import_node.InsertTextFormat.PlainText,
12470
+ // タグ補完の後に表示(5000番台)
12471
+ sortText: String(5e3 + index).padStart(6, "0")
12472
+ };
12473
+ });
12474
+ }
12475
+ function createStorageCompletions(tagName) {
12476
+ const category = TAG_STORAGE_MAPPING.get(tagName);
12477
+ if (!category) return [];
12478
+ const files = scanner.getAssetsForCategory(category);
12479
+ return files.map((file, index) => ({
12480
+ label: file,
12481
+ kind: import_node.CompletionItemKind.File,
12482
+ detail: `${category}/`,
12483
+ sortText: String(index).padStart(4, "0")
12484
+ }));
12485
+ }
12486
+ function createTargetCompletions() {
12487
+ const labels = scanner.getLabels();
12488
+ return labels.map((label, index) => ({
12489
+ label: `*${label.name}`,
12490
+ kind: import_node.CompletionItemKind.Reference,
12491
+ detail: label.file,
12492
+ documentation: {
12493
+ kind: import_node.MarkupKind.Markdown,
12494
+ value: `\u30E9\u30D9\u30EB ***${label.name}**
12495
+
12496
+ \u5B9A\u7FA9\u5143: \`${label.file}\` (\u884C ${label.line + 1})`
12497
+ },
12498
+ sortText: String(index).padStart(4, "0")
12499
+ }));
12500
+ }
12192
12501
  connection.onCompletion(
12193
12502
  (params) => {
12194
12503
  const document = documents.get(params.textDocument.uri);
@@ -12199,7 +12508,19 @@ connection.onCompletion(
12199
12508
  });
12200
12509
  const trigger = isTagNameTrigger(line, params.position.character);
12201
12510
  if (trigger) {
12202
- return createTagCompletions(trigger);
12511
+ const tagItems = createTagCompletions(trigger);
12512
+ const macroItems = createMacroCompletions(trigger);
12513
+ return [...tagItems, ...macroItems];
12514
+ }
12515
+ const valueCtx = getParamValueContext(line, params.position.character);
12516
+ if (valueCtx) {
12517
+ if (valueCtx.paramName === "storage") {
12518
+ return createStorageCompletions(valueCtx.tagName);
12519
+ }
12520
+ if (valueCtx.paramName === "target") {
12521
+ return createTargetCompletions();
12522
+ }
12523
+ return [];
12203
12524
  }
12204
12525
  const context = getTagContext(line, params.position.character);
12205
12526
  if (context && context.isInParams) {
@@ -12333,5 +12654,19 @@ ${param.description}`
12333
12654
  }
12334
12655
  return null;
12335
12656
  }
12657
+ var updateTimers = /* @__PURE__ */ new Map();
12658
+ documents.onDidChangeContent((change) => {
12659
+ const uri = change.document.uri;
12660
+ if (!uri.endsWith(".ks")) return;
12661
+ const existing = updateTimers.get(uri);
12662
+ if (existing) clearTimeout(existing);
12663
+ updateTimers.set(
12664
+ uri,
12665
+ setTimeout(() => {
12666
+ scanner.updateFile(uri, change.document.getText());
12667
+ updateTimers.delete(uri);
12668
+ }, 500)
12669
+ );
12670
+ });
12336
12671
  documents.listen(connection);
12337
12672
  connection.listen();
package/package.json CHANGED
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "@void2610/tyranoscript-lsp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "TyranoScript Language Server for Zed",
5
5
  "main": "dist/server.js",
6
- "files": ["dist/"],
6
+ "files": [
7
+ "dist/"
8
+ ],
7
9
  "scripts": {
8
10
  "build": "esbuild src/server.ts --bundle --platform=node --outfile=dist/server.js --format=cjs",
9
11
  "watch": "esbuild src/server.ts --bundle --platform=node --outfile=dist/server.js --format=cjs --watch",