首页 > 开发工具 > VSCode > 正文

如何为VSCode设置一个自定义的语义标记提供程序?

狼影
发布: 2025-10-14 11:41:01
原创
829人浏览过
答案:通过编写语言服务器并集成LSP协议,可为VS Code添加自定义语义标记;需在package.json中定义token类型,使用vscode-languageserver-node等库实现服务器逻辑,并优化性能以处理大型文件。

如何为vscode设置一个自定义的语义标记提供程序?

为 VS Code 设置自定义语义标记提供程序,核心在于扩展 VS Code 的语言服务能力,让编辑器能更智能地理解你的代码。这需要你编写一个语言服务器,并将其与 VS Code 集成。

解决方案

  1. 选择合适的语言服务器协议(LSP)库: LSP 定义了编辑器和语言服务器之间的通信协议。有多种 LSP 库可供选择,例如:

    • Node.js: vscode-languageserver-node (官方库)
    • Python: pygls
    • Java: lsp4j

    选择你熟悉的语言和对应的库。这里以 Node.js 和 vscode-languageserver-node 为例。

  2. 创建语言服务器:

    • 安装依赖:
    npm install vscode-languageserver vscode-languageserver-protocol vscode-uri
    登录后复制
    • 编写服务器代码 (server.ts):
    import {
        createConnection,
        TextDocuments,
        Diagnostic,
        DiagnosticSeverity,
        ProposedFeatures,
        InitializeParams,
        DidChangeConfigurationNotification,
        CompletionItem,
        CompletionItemKind,
        TextDocumentPositionParams,
        TextDocumentSyncKind,
        InitializeResult,
        SemanticTokensBuilder,
        SemanticTokensLegend,
        SemanticTokensParams
    } from 'vscode-languageserver/node';
    import { TextDocument } from 'vscode-languageserver-textdocument';
    
    // 创建连接和文档管理器
    const connection = createConnection(ProposedFeatures.all);
    const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
    
    let hasConfigurationCapability: boolean = false;
    let hasWorkspaceFolderCapability: boolean = false;
    let hasDiagnosticRelatedInformationCapability: boolean = false;
    
    connection.onInitialize((params: InitializeParams) => {
        const capabilities = params.capabilities;
    
        hasConfigurationCapability = !!(
            capabilities.workspace && !!capabilities.workspace.configuration
        );
        hasWorkspaceFolderCapability = !!(
            capabilities.workspace && !!capabilities.workspace.workspaceFolders
        );
        hasDiagnosticRelatedInformationCapability = !!(
            capabilities.textDocument &&
            capabilities.textDocument.publishDiagnostics &&
            capabilities.textDocument.publishDiagnostics.relatedInformation
        );
    
        const result: InitializeResult = {
            capabilities: {
                textDocumentSync: TextDocumentSyncKind.Incremental,
                completionProvider: {
                    resolveProvider: true
                },
                semanticTokensProvider: {
                    legend: {
                        tokenTypes: ['class', 'interface', 'enum', 'function', 'variable'],
                        tokenModifiers: []
                    },
                    range: false,
                    full: true
                }
            }
        };
        if (hasWorkspaceFolderCapability) {
            result.capabilities.workspace = {
                workspaceFolders: {
                    supported: true
                }
            };
        }
        return result;
    });
    
    connection.onInitialized(() => {
        if (hasConfigurationCapability) {
            connection.client.register(DidChangeConfigurationNotification.type, undefined);
        }
        if (hasWorkspaceFolderCapability) {
            connection.workspace.onDidChangeWorkspaceFolders(_event => {
                connection.console.log('Workspace folder change event received.');
            });
        }
    });
    
    interface ExampleSettings {
        maxNumberOfProblems: number;
    }
    
    const defaultSettings: ExampleSettings = { maxNumberOfProblems: 1000 };
    let globalSettings: ExampleSettings = defaultSettings;
    
    const documentSettings: Map<string, Thenable<ExampleSettings>> = new Map();
    
    connection.onDidChangeConfiguration(change => {
        if (hasConfigurationCapability) {
            documentSettings.clear();
        } else {
            globalSettings = <ExampleSettings>(
                (change.settings.languageServerExample || defaultSettings)
            );
        }
    
        documents.all().forEach(validateTextDocument);
    });
    
    function getDocumentSettings(resource: string): Thenable<ExampleSettings> {
        if (!hasConfigurationCapability) {
            return Promise.resolve(globalSettings);
        }
        let result = documentSettings.get(resource);
        if (!result) {
            result = connection.workspace.getConfiguration({
                scopeUri: resource,
                section: 'languageServerExample'
            });
            documentSettings.set(resource, result);
        }
        return result;
    }
    
    async function validateTextDocument(textDocument: TextDocument): Promise<void> {
        const settings = await getDocumentSettings(textDocument.uri);
    
        const text = textDocument.getText();
        const pattern = /\b[A-Z][a-z]+\b/g;
        let m: RegExpExecArray | null;
    
        let problems = 0;
        const diagnostics: Diagnostic[] = [];
        while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
            problems++;
            const diagnostic: Diagnostic = {
                severity: DiagnosticSeverity.Warning,
                range: {
                    start: textDocument.positionAt(m.index),
                    end: textDocument.positionAt(m.index + m[0].length)
                },
                message: `${m[0]} is using PascalCase.`,
                source: 'ex'
            };
            if (hasDiagnosticRelatedInformationCapability) {
                diagnostic.relatedInformation = [
                    {
                        location: {
                            uri: textDocument.uri,
                            range: Object.assign({}, diagnostic.range)
                        },
                        message: 'Spelling matters'
                    }
                ];
            }
            diagnostics.push(diagnostic);
        }
    
        connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
    }
    
    documents.onDidChangeContent(change => {
        validateTextDocument(change.document);
    });
    
    connection.onDidChangeWatchedFiles(_change => {
        connection.console.log('We received an file change event');
    });
    
    connection.onCompletion(
        (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
            return [
                {
                    label: 'TypeScript',
                    kind: CompletionItemKind.Text,
                    data: 1
                },
                {
                    label: 'JavaScript',
                    kind: CompletionItemKind.Text,
                    data: 2
                }
            ];
        }
    );
    
    connection.onCompletionResolve(
        (item: CompletionItem): CompletionItem => {
            if (item.data === 1) {
                item.detail = 'TypeScript details';
                item.documentation = 'Documentation for TypeScript';
            } else if (item.data === 2) {
                item.detail = 'JavaScript details';
                item.documentation = 'Documentation for JavaScript';
            }
            return item;
        }
    );
    
    connection.onSemanticTokens((params: SemanticTokensParams) => {
        const builder = new SemanticTokensBuilder();
        const text = documents.get(params.textDocument.uri)?.getText();
        if (!text) {
            return { data: [] };
        }
    
        // 示例:标记所有 "class" 关键字为 "class" tokenType
        let match: RegExpExecArray | null;
        const regex = /\bclass\b/g;
        while ((match = regex.exec(text)) !== null) {
            const start = match.index;
            const length = match[0].length;
            const position = documents.get(params.textDocument.uri)?.positionAt(start);
            if (position) {
                builder.push(
                    position.line,
                    position.character,
                    length,
                    0, // tokenType (0 corresponds to 'class' in the legend)
                    0  // tokenModifiers (none)
                );
            }
        }
    
        return builder.build();
    });
    
    documents.listen(connection);
    connection.listen();
    登录后复制
    • 编译 TypeScript: tsc server.ts --target es6 --module commonjs --outDir out
  3. 创建 VS Code 扩展:

    • 创建扩展目录: mkdir my-extension && cd my-extension
    • 初始化扩展: yo code (需要安装 npm install -g yo generator-code)
    • 选择 "New Language Server"`
    • 配置扩展 (package.json):
    {
        "name": "my-extension",
        "displayName": "My Extension",
        "description": "A language server example",
        "version": "0.0.1",
        "engines": {
            "vscode": "^1.63.0"
        },
        "categories": [
            "Languages"
        ],
        "activationEvents": [
            "onLanguage:yourLanguageId"
        ],
        "main": "./client/out/extension",
        "contributes": {
            "languages": [
                {
                    "id": "yourLanguageId",
                    "aliases": [
                        "Your Language",
                        "yourLanguageId"
                    ],
                    "extensions": [
                        ".yourExtension"
                    ],
                    "configuration": "./language-configuration.json"
                }
            ],
            "configurationDefaults": {
                "[yourLanguageId]": {}
            },
            "semanticTokenTypes": [
                "class",
                "interface",
                "enum",
                "function",
                "variable"
            ],
            "semanticTokenScopes": [
                {
                    "language": "yourLanguageId",
                    "scopes": {
                        "class": [ "entity.name.class.yourLanguageId" ],
                        "interface": [ "entity.name.interface.yourLanguageId" ],
                        "enum": [ "entity.name.enum.yourLanguageId" ],
                        "function": [ "entity.name.function.yourLanguageId" ],
                        "variable": [ "variable.other.yourLanguageId" ]
                    }
                }
            ]
        },
        "dependencies": {
            "@types/vscode": "^1.63.0",
            "vscode-languageclient": "^8.0.0",
            "vscode-languageserver": "^8.0.0",
            "vscode-languageserver-protocol": "^3.17.0"
        },
        "devDependencies": {
            "@types/glob": "^7.1.4",
            "@types/mocha": "^9.0.0",
            "@types/node": "16.x",
            "eslint": "^8.4.1",
            "glob": "^7.1.7",
            "mocha": "^9.1.3",
            "typescript": "^4.5.4",
            "@vscode/test-electron": "^2.0.3"
        }
    }
    登录后复制
    • 修改客户端代码 (client/src/extension.ts):
    import * as path from 'path';
    import { workspace, ExtensionContext } from 'vscode';
    
    import {
        LanguageClient,
        LanguageClientOptions,
        ServerOptions,
        TransportKind
    } from 'vscode-languageclient/node';
    
    let client: LanguageClient;
    
    export function activate(context: ExtensionContext) {
        const serverModule = context.asAbsolutePath(
            path.join('server', 'out', 'server.js')
        );
    
        const debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };
    
        const serverOptions: ServerOptions = {
            run: { module: serverModule, transport: TransportKind.ipc },
            debug: {
                module: serverModule,
                transport: TransportKind.ipc,
                options: debugOptions
            }
        };
    
        const clientOptions: LanguageClientOptions = {
            documentSelector: [{ scheme: 'file', language: 'yourLanguageId' }],
            synchronize: {
                configurationSection: 'languageServerExample',
                fileEvents: workspace.createFileSystemWatcher('**/.clientrc')
            }
        };
    
        client = new LanguageClient(
            'languageServerExample',
            'Language Server Example',
            serverOptions,
            clientOptions
        );
    
        client.start();
    }
    
    export function deactivate(): Thenable<void> | undefined {
        if (!client) {
            return undefined;
        }
        return client.stop();
    }
    登录后复制
  4. 配置语言:

    • 创建 language-configuration.json 文件,定义语言的语法和符号。
    {
        "comments": {
            "lineComment": "//",
            "blockComment": [ "/*", "*/" ]
        },
        "brackets": [
            ["{", "}"],
            ["[", "]"],
            ["(", ")"]
        ],
        "autoClosingPairs": [
            {"open": "{", "close": "}"},
            {"open": "[", "close": "]"},
            {"open": "(", "close": ")"},
            {"open": "\"", "close": "\"", "notIn": ["string"]},
            {"open": "'", "close": "'", "notIn": ["string", "comment"]}
        ],
        "surroundingPairs": [
            ["{", "}"],
            ["[", "]"],
            ["(", ")"],
            ["\"", "\""],
            ["'", "'"]
        ]
    }
    登录后复制
  5. 调试和测试:

    • 使用 VS Code 的调试功能调试语言服务器和扩展。
    • 编写测试用例来验证语义标记的正确性。

如何定义自定义的token类型和修饰符?

package.json文件的contributes.semanticTokenTypes部分,你可以定义自己的token类型。 例如,你可以添加一个名为myCustomType的token类型:

"contributes": {
    "semanticTokenTypes": [
        "class",
        "interface",
        "enum",
        "function",
        "variable",
        "myCustomType"
    ]
}
登录后复制

然后在你的语言服务器代码中,你需要确保你使用了这个新的token类型。 在server.tsconnection.onSemanticTokens处理程序中,你需要修改builder.push调用,以使用正确的token类型索引。 例如,如果myCustomType是列表中的第6个类型(索引为5),那么你需要使用5作为tokenType参数。

如何处理大型文件以提高语义标记的性能?

处理大型文件进行语义标记可能会变得非常慢。 以下是一些提高性能的策略:

  1. 增量更新: 只在文件更改的部分重新计算语义标记,而不是每次都重新处理整个文件。 使用TextDocument.onDidChangeContent事件来检测更改,并仅对更改的区域进行标记。

    ShopWind网店系统
    ShopWind网店系统

    ShopWind网店系统是国内最专业的网店程序之一,采用ASP语言设计开发,速度快、性能好、安全性高。ShopWind网店购物系统提供性化的后台管理界面,标准的网上商店管理模式和强大的网店软件后台管理功能。ShopWind网店系统提供了灵活强大的模板机制,内置多套免费精美模板,同时可在后台任意更换,让您即刻快速建立不同的网店外观。同时您可以对网模板自定义设计,建立个性化网店形象。ShopWind网

    ShopWind网店系统 0
    查看详情 ShopWind网店系统
  2. 分块处理: 将文件分成更小的块,并并行处理这些块。 这可以通过使用Web Workers或Node.js的cluster模块来实现。

  3. 缓存: 缓存语义标记的结果,以便在下次需要时可以重用它们。 确保在文件更改时使缓存失效。

  4. 优化正则表达式: 确保你使用的正则表达式是高效的。 避免使用过于复杂的正则表达式,并尽可能使用预编译的正则表达式。

  5. 限制标记范围: 只标记当前可见区域内的代码。 当用户滚动到新的区域时,再标记新的代码。

如何在不同的编程语言中实现语义标记提供程序?

不同的编程语言有不同的LSP库和工具。 以下是一些常用语言的例子:

  • Python: 使用pygls库。 pygls提供了一个简单的API来创建语言服务器。 你可以使用pygls.lsp.methods.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL方法来注册语义标记提供程序。

  • Java: 使用lsp4j库。 lsp4j是一个用于LSP的Java绑定。 你需要创建一个实现TextDocumentService接口的类,并实现semanticTokensFull方法。

  • C#: 使用OmniSharpMonoDevelop。 这些工具提供对C#语言的LSP支持。

无论你选择哪种语言,都需要遵循LSP协议,并使用相应的库来处理与VS Code的通信。 关键在于理解LSP协议,并能够将你的语言的语法和语义映射到LSP的token类型和修饰符。

以上就是如何为VSCode设置一个自定义的语义标记提供程序?的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号