vscode插件开发心得:RustedWarfareModSupport

发布时间:
更新时间:

我并没有重复造轮子,因为根本就没轮子 :(

时隔多年,我还是回到了我最喜欢的RTS游戏:Steam 上的 Rusted Warfare - RTS

这是我的插件地址 RustedWarfareModSupport - Visual Studio Marketplace

开发过程

起步,想使用yo code?

这脚手架有点太简陋了,所以我直接git clone 一下微软官方的lsp-sample插件来改一改,顺带学习一下lsp

:::tip 什么是lsp? LSP 通常指 ​ 语言服务器协议(Language Server Protocol) ​,是一种用于在集成开发环境(IDE)和语言服务器之间通信的开放标准。它通过标准化的请求和响应机制,让开发者工具(如 VS Code、Emacs)与语言支持服务(如代码补全、语法检查、重构工具)解耦,从而实现跨平台、跨编辑器的语言功能支持。 :::

果然官方的就是好啊,注释什么的都一应俱全,但为什么不把这个作为yo code的选项之一呢?我还是不太明白 不过,既然有了模板,那就开始改造!

代码表,数据从哪来?

倒是能看到许多excal表格,但这不是一个对js/ts友好的数据结构,找了一下,看到了一个插件仓库

Blackburn507/RWini_Plugin: A plugin improves coding for MODs of a RTS game Rusted Warfare.

但是我好奇为什么没在vscode插件市场上看到,看了一下是MIT协议,里面的/db/文件夹有三个数据json,看起来不方便,于是我开始拆分

拆分dataVAseKey.json

const fs = require('fs');
const path = require('path');
// 读取 dataBaseKey.json 文件内容
const dataBaseKeyJson = fs.readFileSync('dataBaseKey.json', 'utf-8');
const dataBaseKey = JSON.parse(dataBaseKeyJson);
// 定义 CompletionItemKind 常量
const CompletionItemKind = {
Text: 1
};
// 定义一个函数来生成 CompletionItem[] 数据
function generateCompletionItems(section, items) {
return Object.entries(items).map(([label, [type, descriptionSection, detail, documentation]], index) => ({
label,
insertText: `${label}:`,
labelDetails: { description: `[${section}]` },
kind: CompletionItemKind.Text,
detail,
documentation,
type
}));
}
// 遍历 dataBaseKey 中的每个 section
for (const [section, items] of Object.entries(dataBaseKey)) {
const completionItems = generateCompletionItems(section, items);
// 生成文件内容
const importStatement = `import { CompletionItem, CompletionItemKind } from 'vscode-languageserver';`;
const exportStatement = `export const ${section.toUpperCase()}: CompletionItem[] = [`;
const itemsContent = completionItems.map(item => ` {
label: '${item.label||''}',
insertText: '${item.insertText||''}',
labelDetails: { detail :' ${(item.type||'')+' '+(item.detail||'').replace(/'/g, "\\'")}', description: '[${section}]' },
kind: CompletionItemKind.Text,
detail :'${(item.detail||'').replace(/'/g, "\\'")}',
documentation :'${(item.documentation||'').replace(/'/g, "\\'")}',
data: '${(item.type||'').replace(/'/g, "\\'")}'
},`).join('\n');
const closingBracket = `];`;
const fileContent = `${importStatement}\n${exportStatement}\n${itemsContent}\n${closingBracket}`;
// 写入文件
const filePath = path.join(__dirname, `${section}.ts`);
fs.writeFileSync(filePath, fileContent, 'utf-8');
}
console.log('文件生成完成');

拆分dataBaseValue.json

const fs = require('fs');
const path = require('path');
// 读取 dataBaseValue.json 文件内容
const dataBaseValueJson = fs.readFileSync('dataBaseValue.json', 'utf-8');
const dataBaseValue = JSON.parse(dataBaseValueJson);
// 定义 CompletionItemKind 常量
const CompletionItemKind = {
Text: 1
};
// 定义一个函数来生成 CompletionItem[] 数据
function generateCompletionItems(section, items) {
return Object.entries(items).map(([label, [type, description, detail]], index) => ({
label,
insertText: `${label}`,
labelDetails: { description: `[${section}]` },
kind: CompletionItemKind.Text,
detail,
documentation: description,
type
}));
}
// 遍历 dataBaseValue 中的每个 section
for (const [section, items] of Object.entries(dataBaseValue)) {
const completionItems = generateCompletionItems(section, items);
// 生成文件内容
const importStatement = `import { CompletionItem, CompletionItemKind } from 'vscode-languageserver';`;
const exportStatement = `export const ${section.toUpperCase()}: CompletionItem[] = [`;
const itemsContent = completionItems.map(item => ` {
label: '${item.label || ''}',
insertText: '${item.insertText || ''}',
labelDetails: { detail :' ${(item.type || '') + ' ' + (item.detail || '').replace(/'/g, "\\'")}', description: '[${section}]' },
kind: CompletionItemKind.Text,
detail :'${(item.detail || '').replace(/'/g, "\\'")}',
documentation :'${(item.documentation || '').replace(/'/g, "\\'")}',
data: '${(item.type || '').replace(/'/g, "\\'")}'
},`).join('\n');
const closingBracket = `];`;
const fileContent = `${importStatement}\n${exportStatement}\n${itemsContent}\n${closingBracket}`;
// 写入文件
const filePath = path.join(__dirname, `${section}.ts`);
fs.writeFileSync(filePath, fileContent, 'utf-8');
}
console.log('文件生成完成');

现在获得了这些文件,对于这些文件,应该定义一下良好的数据结构

定义Completionitem[]

定义这个对象数组,方便补全,下面是其中的一个例子

import { CompletionItem, CompletionItemKind } from 'vscode-languageserver';
export const ACTION: CompletionItem[] = [
{
label: 'text',
insertText: 'text:',
labelDetails: { detail :' file-text 文本', description: '[action]' },
kind: CompletionItemKind.Text,
detail :'文本',
documentation :'界面中显示的文字',
data: 'file-text'
},
{
label: 'textPostFix',
insertText: 'textPostFix:',
labelDetails: { detail :' file-text 文本动态更改', description: '[action]' },
kind: CompletionItemKind.Text,
detail :'文本动态更改',
documentation :'显示为后缀的文本,与textAddUnitName一起用于创建文本UI',
data: 'file-text'
},
...
]

合在一起,方便调用

import { CompletionItem } from 'vscode-languageserver';
import { ACTION } from './action';
import { AI } from './ai';
import { ANIMATION } from './animation';
import { ATTACHMENT } from './attachment';
import { ATTACK } from './attack';
import { CANBUILD } from './canBuild';
import { CORE } from './core';
import { EFFECT } from './effect';
import { GRAPHICS } from './graphics';
import { LEG } from './leg';
import { MOVEMENT } from './movement';
import { PLACEMENTRULE } from './placementRule';
import { PROJECTILE } from './projectile';
import { RESOURCE } from './resource';
import { TURRET } from './turret';
export const ALLSECTIONS: Record<string, CompletionItem[]> ={
ACTION,
AI,
ANIMATION,
ATTACHMENT,
ATTACK,
CANBUILD,
CORE,
EFFECT,
GRAPHICS,
LEG,
MOVEMENT,
PLACEMENTRULE,
PROJECTILE,
RESOURCE,
TURRET
};

关键在于connection.onCompletion函数的调用

connection.onCompletion(
(_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
const document = documents.get(_textDocumentPosition.textDocument.uri);
if (!document) {return [];}
// 获取当前行号
const cursorLine = _textDocumentPosition.position.line;
// 从当前行向上查找最近的一个节定义
let currentSectionName = null;
for (let i = cursorLine; i >= 0; i--) {
const lineStart = document.positionAt(document.offsetAt({ line: i, character: 0 }));
const lineEnd = document.positionAt(document.offsetAt({ line: i + 1, character: 0 }) - 1);
const lineContent = document.getText({ start: lineStart, end: lineEnd }).trim();
const sectionMatch = lineContent.match(/^\[(.+)\]$/);
if (sectionMatch) {
currentSectionName = sectionMatch[1];
break;
}
}
const cursorCharacter = _textDocumentPosition.position.character;
// 获取当前行内容
const lineStart = document.positionAt(document.offsetAt({ line: cursorLine, character: 0 }));
const lineEnd = document.positionAt(document.offsetAt({ line: cursorLine + 1, character: 0 }) - 1);
const currentLineContent = document.getText({ start: lineStart, end: lineEnd });
// 判断光标是否在 [] 内
const openBracketIndex = currentLineContent.lastIndexOf('[', cursorCharacter);
const closeBracketIndex = currentLineContent.indexOf(']', cursorCharacter);
const isInsideBrackets = openBracketIndex !== -1 && (closeBracketIndex === -1 || openBracketIndex > closeBracketIndex);
if (isInsideBrackets) {
return SECTIONSNAME;
}
// 判断光标是否在 :后
const colonIndex = currentLineContent.indexOf(':');
const pointIndex = currentLineContent.indexOf('.');
if (!currentSectionName) {
return [];
}
else if((colonIndex!==-1)&&(cursorCharacter>colonIndex)){
if((pointIndex!==-1)&&(cursorCharacter==(pointIndex+1))){
return ALLVALUES.UNITPROPERTY;
}
const key = currentLineContent.substring(0, colonIndex).trim();
let sectionName=currentSectionName;
sectionName=sectionName.toUpperCase();
const sectionConfig = ALLSECTIONS[sectionName];
let keyConfig = null;
if (sectionConfig) {
for (const item of sectionConfig) {
if (item.label === key) {
keyConfig = item;
break;
}
}
}
if (keyConfig) {
const data = keyConfig.data;
if (data.includes('bool')) {
return ALLVALUES.BOOL;
} else if (data.includes('logicBoolean')) {
return [...ALLVALUES.LOGICBBOOLEAN, ...ALLVALUES.BOOL,...ALLVALUES.FUNCTION,...ALLVALUES.UNITREF];
} else if (key.includes('spawnUnits')){
return [...ALLVALUES.LOGICBBOOLEAN,...ALLVALUES.BOOL,...ALLVALUES.FUNCTION,...ALLVALUES.UNITREF,...ALLVALUES.SPAWNUNIT];
} else if (data.includes('projectileRef')){
return [...ALLVALUES.PROJECTILE];
} else if (data.includes('event')){
return [...ALLVALUES.EVENTS];
}
}
return [];;
}
else {
// 根据节名返回相应的补全项
switch (currentSectionName) {
case 'core':
return ALLSECTIONS.CORE;
case 'graphics':
return ALLSECTIONS.GRAPHICS;
case 'attack':
return ALLSECTIONS.ATTACK;
case 'movement':
return ALLSECTIONS.MOVEMENT;
case 'ai':
return ALLSECTIONS.AI;
default:
if (currentSectionName.startsWith('canBuild_')) {
return ALLSECTIONS.CANBUILD;
} else if (currentSectionName.startsWith('turret_')) {
return ALLSECTIONS.TURRET;
} else if (currentSectionName.startsWith('projectile_')){
return ALLSECTIONS.PROJECTILE;
} else if (currentSectionName.startsWith('arm_')){
return ALLSECTIONS.ARM;
} else if (currentSectionName.startsWith('leg_')){
return ALLSECTIONS.LEG;
} else if (currentSectionName.startsWith('attachment_')){
return ALLSECTIONS.ATTACHMENT;
} else if (currentSectionName.startsWith('effect_')){
return ALLSECTIONS.EFFECT;
} else if (currentSectionName.startsWith('animation_')){
return ALLSECTIONS.ANIMATION;
} else if (currentSectionName.startsWith('action_')){
return ALLSECTIONS.ACTION;
} else if (currentSectionName.startsWith('hiddenAction_')){
return ALLSECTIONS.ACTION;
} else if (currentSectionName.startsWith('placementRule_')){
return ALLSECTIONS.PLACEMENTRULE;
} else if (currentSectionName.startsWith('resource_')){
return ALLSECTIONS.RESOURCE;
} else if (currentSectionName.startsWith('global_resource_')){
return ALLSECTIONS.RESOURCE;
} else if (currentSectionName.startsWith('decal_')){
return ALLSECTIONS.DECAL;
} else if (currentSectionName.startsWith('commnet_')){
return [];
} else if (currentSectionName.startsWith('template_')){
return ALLSECTIONS.TEMPLATE;
}
return [];
}
}
return [];
});

太长了不爱看?让我们分开来解析一下!

代码功能概述

这段代码是一个用于处理代码补全的函数,通过监听 connection 对象的 onCompletion 事件,根据用户在文本编辑器中的光标位置和上下文信息,提供相应的代码补全项。具体来说,它会根据当前所在的节(section)、光标位置是否在方括号内或冒号之后等条件,返回不同的补全项数组。

代码详细分析

1. 事件监听与回调函数

javascript

connection.onCompletion(
(_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
// 代码主体
}
);
  • connection.onCompletion 是一个事件监听器,用于监听代码补全请求事件。
  • 回调函数接收一个 TextDocumentPositionParams 类型的参数 _textDocumentPosition,该参数包含了当前文本文件的位置信息。
  • 回调函数返回一个 CompletionItem 类型的数组,用于提供代码补全项。

2. 获取当前文档

javascript

const document = documents.get(_textDocumentPosition.textDocument.uri);
if (!document) { return []; }
  • 通过 documents.get 方法根据当前文本文件的 URI 获取对应的文档对象。
  • 如果文档对象不存在,则返回一个空数组,表示没有补全项。

3. 获取当前行号

javascript

const cursorLine = _textDocumentPosition.position.line;
  • _textDocumentPosition 参数中获取当前光标的行号。

4. 查找最近的节定义

javascript

let currentSectionName = null;
for (let i = cursorLine; i >= 0; i--) {
const lineStart = document.positionAt(document.offsetAt({ line: i, character: 0 }));
const lineEnd = document.positionAt(document.offsetAt({ line: i + 1, character: 0 }) - 1);
const lineContent = document.getText({ start: lineStart, end: lineEnd }).trim();
const sectionMatch = lineContent.match(/^\[(.+)\]$/);
if (sectionMatch) {
currentSectionName = sectionMatch[1];
break;
}
}
  • 从当前行开始向上遍历,查找最近的一个节定义(以 [节名] 形式表示)。
  • 使用正则表达式 /^\[(.+)\]$/ 匹配节定义,如果匹配成功,则将节名存储在 currentSectionName 变量中,并跳出循环。

5. 获取当前光标位置和行内容

javascript

const cursorCharacter = _textDocumentPosition.position.character;
const lineStart = document.positionAt(document.offsetAt({ line: cursorLine, character: 0 }));
const lineEnd = document.positionAt(document.offsetAt({ line: cursorLine + 1, character: 0 }) - 1);
const currentLineContent = document.getText({ start: lineStart, end: lineEnd });
  • 获取当前光标的列号 cursorCharacter
  • 获取当前行的起始位置和结束位置,并提取当前行的内容 currentLineContent

6. 判断光标是否在方括号内

javascript

const openBracketIndex = currentLineContent.lastIndexOf('[', cursorCharacter);
const closeBracketIndex = currentLineContent.indexOf(']', cursorCharacter);
const isInsideBrackets = openBracketIndex !== -1 && (closeBracketIndex === -1 || openBracketIndex > closeBracketIndex);
if (isInsideBrackets) {
return SECTIONSNAME;
}
  • 使用 lastIndexOfindexOf 方法分别查找当前行中最后一个左方括号和第一个右方括号的位置。
  • 根据左方括号和右方括号的位置判断光标是否在方括号内。
  • 如果光标在方括号内,则返回 SECTIONSNAME 数组作为补全项。

7. 判断光标是否在冒号之后

javascript

const colonIndex = currentLineContent.indexOf(':');
const pointIndex = currentLineContent.indexOf('.');
if (!currentSectionName) {
return [];
} else if ((colonIndex!== -1) && (cursorCharacter > colonIndex)) {
if ((pointIndex!== -1) && (cursorCharacter == (pointIndex + 1))) {
return ALLVALUES.UNITPROPERTY;
}
const key = currentLineContent.substring(0, colonIndex).trim();
let sectionName = currentSectionName;
sectionName = sectionName.toUpperCase();
const sectionConfig = ALLSECTIONS[sectionName];
let keyConfig = null;
if (sectionConfig) {
for (const item of sectionConfig) {
if (item.label === key) {
keyConfig = item;
break;
}
}
}
if (keyConfig) {
const data = keyConfig.data;
if (data.includes('bool')) {
return ALLVALUES.BOOL;
} else if (data.includes('logicBoolean')) {
return [...ALLVALUES.LOGICBBOOLEAN, ...ALLVALUES.BOOL, ...ALLVALUES.FUNCTION, ...ALLVALUES.UNITREF];
} else if (key.includes('spawnUnits')) {
return [...ALLVALUES.LOGICBBOOLEAN, ...ALLVALUES.BOOL, ...ALLVALUES.FUNCTION, ...ALLVALUES.UNITREF, ...ALLVALUES.SPAWNUNIT];
} else if (data.includes('projectileRef')) {
return [...ALLVALUES.PROJECTILE];
} else if (data.includes('event')) {
return [...ALLVALUES.EVENTS];
}
}
return [];
}
  • 查找当前行中冒号和点号的位置。
  • 如果没有找到节名,则返回一个空数组。
  • 如果光标在冒号之后,进一步判断:
    • 如果光标在点号之后,则返回 ALLVALUES.UNITPROPERTY 数组作为补全项。
    • 提取冒号之前的键名,并将节名转换为大写。
    • 根据节名从 ALLSECTIONS 对象中获取对应的配置信息。
    • 在配置信息中查找与键名匹配的项。
    • 根据匹配项的 data 属性,返回不同的补全项数组。

8. 根据节名返回补全项

javascript

else {
switch (currentSectionName) {
case 'core':
return ALLSECTIONS.CORE;
case 'graphics':
return ALLSECTIONS.GRAPHICS;
case 'attack':
return ALLSECTIONS.ATTACK;
case 'movement':
return ALLSECTIONS.MOVEMENT;
case 'ai':
return ALLSECTIONS.AI;
default:
if (currentSectionName.startsWith('canBuild_')) {
return ALLSECTIONS.CANBUILD;
} else if (currentSectionName.startsWith('turret_')) {
return ALLSECTIONS.TURRET;
} else if (currentSectionName.startsWith('projectile_')) {
return ALLSECTIONS.PROJECTILE;
} else if (currentSectionName.startsWith('arm_')) {
return ALLSECTIONS.ARM;
} else if (currentSectionName.startsWith('leg_')) {
return ALLSECTIONS.LEG;
} else if (currentSectionName.startsWith('attachment_')) {
return ALLSECTIONS.ATTACHMENT;
} else if (currentSectionName.startsWith('effect_')) {
return ALLSECTIONS.EFFECT;
} else if (currentSectionName.startsWith('animation_')) {
return ALLSECTIONS.ANIMATION;
} else if (currentSectionName.startsWith('action_')) {
return ALLSECTIONS.ACTION;
} else if (currentSectionName.startsWith('hiddenAction_')) {
return ALLSECTIONS.ACTION;
} else if (currentSectionName.startsWith('placementRule_')) {
return ALLSECTIONS.PLACEMENTRULE;
} else if (currentSectionName.startsWith('resource_')) {
return ALLSECTIONS.RESOURCE;
} else if (currentSectionName.startsWith('global_resource_')) {
return ALLSECTIONS.RESOURCE;
} else if (currentSectionName.startsWith('decal_')) {
return ALLSECTIONS.DECAL;
} else if (currentSectionName.startsWith('commnet_')) {
return [];
} else if (currentSectionName.startsWith('template_')) {
return ALLSECTIONS.TEMPLATE;
}
return [];
}
}
  • 如果光标不在方括号内也不在冒号之后,则根据节名返回不同的补全项数组。
  • 使用 switch 语句处理一些常见的节名,如 coregraphics 等。
  • 对于以特定前缀开头的节名,也返回相应的补全项数组。
  • 如果节名不匹配任何条件,则返回一个空数组。

9. 默认返回值

javascript

return [];
  • 如果以上所有条件都不满足,则返回一个空数组,表示没有补全项。

总结

写的我有点神游了,本来想any一把梭的,但是想了想还是规范一下吧. 造轮子果然很难,但写完之后用轮子方便了不少,感慨那些写代码提示的人有多辛苦 这学期要学编译原理,汇编语言,不知道这对我的插件开发会不会有更好的帮助呢?

留言评论

2000年1月1日星期六
00:00:00