首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >vscode插件开发心得:RustedWarfareModSupport

vscode插件开发心得:RustedWarfareModSupport

作者头像
姓王者
发布2025-03-10 09:32:09
发布2025-03-10 09:32:09
1860
举报
文章被收录于专栏:姓王者的博客姓王者的博客

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

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

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

开发过程

起步,想使用yo code?

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

💡

什么是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
代码语言:javascript
复制
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
代码语言:javascript
复制
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[]

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

代码语言:javascript
复制
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'
    },
...
    ]

合在一起,方便调用

代码语言:javascript
复制
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函数的调用

代码语言:javascript
复制
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

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

javascript

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

javascript

代码语言:javascript
复制
const cursorLine = _textDocumentPosition.position.line;
  • _textDocumentPosition 参数中获取当前光标的行号。
4. 查找最近的节定义

javascript

代码语言: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

代码语言: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

代码语言: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

代码语言: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

代码语言: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

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

总结

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-03-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 开发过程
    • 起步,想使用yo code?
    • 代码表,数据从哪来?
      • 拆分dataVAseKey.json
      • 拆分dataBaseValue.json
      • 定义Completionitem[]
    • 关键在于connection.onCompletion函数的调用
    • 代码功能概述
    • 代码详细分析
      • 1. 事件监听与回调函数
      • 2. 获取当前文档
      • 3. 获取当前行号
      • 4. 查找最近的节定义
      • 5. 获取当前光标位置和行内容
      • 6. 判断光标是否在方括号内
      • 7. 判断光标是否在冒号之后
      • 8. 根据节名返回补全项
      • 9. 默认返回值
    • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档