本文正在参加「金石计划 . 瓜分6万现金大奖」
相关阅读:
前言
在上一篇 Dart 怎么阻止你的同事使用Getx(自定义Lint) - 掘金 (juejin.cn) 中,我们讲到了如何利用 analyzer_plugin 来自定义 Lint, 其实 analyzer_plugin 也能完成一些其他功能(当然我们对这些功能都觉得习以为常),我们一起看看这些的功能是如何完成的。
比如点击代码块,看到各种快捷方式。
这个大家应该经常使用到
代码完成建议,日常必使用的功能。
帮助识别类,字段,属性的全部关联,你可以通过 Change All Occurences 来对某个目标的全部关联进行修改。
这样看来,我们平时习以为常的功能,原来可以通过这些方法来实现,有点意思。那么 Dart Team 是如何使用一套代码在不同的编辑器(AndroidStudio,IntelliJ,VSCode)完成这些功能的呢?
Analysis Server
sdk/pkg/analysis_server at master · dart-lang/sdk (github.com)
Analysis Server 是官方提供的 Dart 语法分析服务,AndroidStudio,IntelliJ,VSCode 都是通过跟它的交互,完成语法静态分析、代码提示、代码补全等功能。
当你配置了 Dart SDK 的环境变量的时候,服务在 Dart SDK 目录下面的 bin/snapshots/analysis_server.dart.snapshot。
而我们一般下载的是 Flutter ,那么它的路径在 Flutter 目录下面的 bin/cache/dart-sdk/bin/snapshots/analysis_server.dart.snapshot
VSCode
VSCode 的插件启动之后,启动一个进程去执行 analysis_server.dart.snapshot。VSCode 的插件负责监听比如用户代码改变,发送消息给 analysis_server,analysis_server 解析之后把结果再告诉 VSCode 的插件,插件通知 VSCode 刷新界面。
export const analyzerSnapshotPath = "bin/snapshots/analysis_server.dart.snapshot";
这里官方还提供了另外一种方式的服务 language-server 。
const analyzerPath = config.analyzerPath || (
dartCapabilities.supportsLanguageServerCommand
? "language-server"
: path.join(sdks.dart, analyzerSnapshotPath)
);
protected createProcess(workingDirectory: string | undefined, binPath: string, args: string[], envOverrides: { envOverrides?: { [key: string]: string | undefined }, toolEnv?: { [key: string]: string | undefined } }) {
this.logTraffic(`Spawning ${binPath} with args ${JSON.stringify(args)}`);
this.description = binPath;
if (workingDirectory)
this.logTraffic(`.. in ${workingDirectory}`);
if (envOverrides.envOverrides || envOverrides.toolEnv)
this.logTraffic(`.. with ${JSON.stringify(envOverrides)}`);
const env = Object.assign({}, envOverrides.toolEnv, envOverrides.envOverrides);
this.process = safeSpawn(workingDirectory, binPath, args, env);
this.logTraffic(` PID: ${process.pid}`);
this.process.stdout.on("data", (data: Buffer | string) => this.handleStdOut(data));
this.process.stderr.on("data", (data: Buffer | string) => this.handleStdErr(data));
this.process.on("exit", (code, signal) => this.handleExit(code, signal));
this.process.on("error", (error) => this.handleError(error));
}
AndroidStudio/IntelliJ
流程跟 VSCode 插件类似, 代码如下。
// 获取到 dart sdk 的位置
mySdkHome = sdk.getHomePath();
// dart 执行文件的位置
// 在 flutter sdk 的 bin/cache/dart-sdk/bin/dart
final String runtimePath = FileUtil.toSystemDependentName(DartSdkUtil.getDartExePath(sdk));
// If true, then the DAS will be started via `dart language-server`, instead of `dart .../analysis_server.dart.snapshot`
// 这里提供了另外一方式的服务 language-server
final boolean useDartLangServerCall = isDartSdkVersionSufficientForDartLangServer(sdk);
// 服务的位置
String analysisServerPath = FileUtil.toSystemDependentName(mySdkHome + "/bin/snapshots/analysis_server.dart.snapshot");
// 配置参数
String firstArgument = useDartLangServerCall ? "language-server" : analysisServerPath;
// Socket 创建
myServerSocket =
new StdioServerSocket(runtimePath, StringUtil.split(vmArgsRaw, " "), firstArgument, StringUtil.split(serverArgsRaw, " "), debugStream);
final RemoteAnalysisServerImpl startedServer = new RemoteAnalysisServerImpl(myServerSocket);
try {
// 启动服务
startedServer.start();
}
其实上你可以试试在你的终端中输入(xxx 为你本地 dart sdk 的路径)
dart xxx/bin/snapshots/analysis_server.dart.snapshot
你将得到下面信息
{"event":"server.connected","params":{"version":"1.33.0","pid":48387}}
当你再输入 {"id":"1","method":"server.getVersion"}(这里不懂没关系,后面会讲这个命令是啥意思。)
你会得到 {"id":"1","result":{"version":"1.33.0"}}
这就是插件悄悄做的事情,那么
AndroidStudio,IntelliJ,VSCode 与 analysis_server 一定是以某种协议进行交互,是什么呢?
LSP
是的,没错,就是你们心里想的那个。输入法输入 lsp,评论区分享下你的是什么。
没错,就是 语言服务协议(Language Server Protocol,简称LSP),官方解释为
The Language Server Protocol (LSP) defines the protocol used between an editor or IDE and a language server that provides language features like auto complete, go to definition, find all references etc. (语言服务器协议是一种被用于编辑器或集成开发环境 与 支持比如自动补全,定义跳转,查找所有引用等语言特性的语言服务器之间的一种协议)
Official page for Language Server Protocol (microsoft.github.io)。
而 Dart analysis server 支持 lsp 的介绍在 sdk/README.md at master · dart-lang/sdk (github.com)
从表格里面,我们可以看到现在支持和未支持的功能。
| Method | Server | Plugins | Notes |
|---|---|---|---|
| initialize | ✅ | N/A | trace and other options NYI |
| initialized | ✅ | N/A | |
| shutdown | ✅ | N/A | supported but does nothing |
| exit | ✅ | N/A | |
| $/cancelRequest | ✅ | ||
| $/logTrace | |||
| $/progress | |||
| $/setTrace | |||
| client/registerCapability | ✅ | ✅ | |
| client/unregisterCapability | ✅ | ✅ | |
| notebookDocument/* | |||
| telemetry/event | |||
| textDocument/codeAction (assists) | ✅ | ✅ | Only if the client advertises codeActionLiteralSupport with Refactor |
| textDocument/codeAction (fixAll) | ✅ | ||
| textDocument/codeAction (fixes) | ✅ | ✅ | Only if the client advertises codeActionLiteralSupport with QuickFix |
| textDocument/codeAction (organiseImports) | ✅ | ||
| textDocument/codeAction (refactors) | ✅ | ||
| textDocument/codeAction (sortMembers) | ✅ | ||
| codeAction/resolve | |||
| textDocument/codeLens | |||
| codeLens/resolve | |||
| textDocument/completion | ✅ | ✅ | |
| completionItem/resolve | ✅ | ||
| textDocument/declaration | |||
| textDocument/definition | ✅ | ✅ | |
| textDocument/diagnostic | |||
| textDocument/didChange | ✅ | ✅ | |
| textDocument/didClose | ✅ | ✅ | |
| textDocument/didOpen | ✅ | ✅ | |
| textDocument/didSave | |||
| textDocument/documentColor | ✅ | ||
| textDocument/colorPresentation | ✅ | ||
| textDocument/documentHighlight | ✅ | ||
| textDocument/documentLink | |||
| documentLink/resolve | |||
| textDocument/documentSymbol | ✅ | ||
| textDocument/foldingRange | ✅ | ✅ | |
| textDocument/formatting | ✅ | ||
| textDocument/onTypeFormatting | ✅ | ||
| textDocument/rangeFormatting | ✅ | ||
| textDocument/hover | ✅ | ||
| textDocument/implementation | ✅ | ||
| textDocument/inlayHint | ✅ | ||
| inlayHint/resolve | |||
| textDocument/inlineValue | |||
| textDocument/linkedEditingRange | |||
| textDocument/moniker | |||
| textDocument/prepareCallHierarchy | ✅ | ||
| callHierarchy/incomingCalls | ✅ | ||
| callHierarchy/outgoingCalls | ✅ | ||
| textDocument/prepareRename | ✅ | ||
| textDocument/rename | ✅ | ||
| textDocument/prepareTypeHierarchy | |||
| typeHierarchy/subtypes | |||
| typeHierarchy/supertypes | |||
| textDocument/publishDiagnostics | ✅ | ✅ | |
| textDocument/references | ✅ | ||
| textDocument/selectionRange | ✅ | ||
| textDocument/semanticTokens/full | ✅ | ✅ | |
| textDocument/semanticTokens/full/delta | |||
| textDocument/semanticTokens/range | ✅ | ✅ | |
| workspace/semanticTokens/refresh | |||
| textDocument/signatureHelp | ✅ | ||
| textDocument/typeDefinition | ✅ | ||
| textDocument/willSave | |||
| textDocument/willSaveWaitUntil | |||
| window/logMessage | ✅ | ||
| window/showDocument | |||
| window/showMessage | ✅ | ||
| window/showMessageRequest | |||
| window/workDoneProgress/cancel | |||
| window/workDoneProgress/create | ✅ | ||
| workspace/applyEdit | ✅ | ||
| workspace/codeLens/refresh | |||
| workspace/configuration | ✅ | ||
| workspace/diagnostic | |||
| workspace/diagnostic/refresh | |||
| workspace/didChangeConfiguration | ✅ | ||
| workspace/didChangeWatchedFiles | unused, server does own watching | ||
| workspace/didChangeWorkspaceFolders | ✅ | ✅ | |
| workspace/didCreateFiles | |||
| workspace/didDeleteFiles | |||
| workspace/didRenameFiles | |||
| workspace/executeCommand | ✅ | ||
| workspace/inlayHint/refresh | |||
| workspace/inlineValue/refresh | |||
| workspace/symbol | ✅ | ||
| workspaceSymbol/resolve | |||
| workspace/willCreateFiles | |||
| workspace/willDeleteFiles | |||
| workspace/willRenameFiles | |||
| workspace/willRenameFiles | ✅ | ||
| workspace/workspaceFolders |
总结,整个流程中,AndroidStudio,IntelliJ,VSCode 插件为中间人,通过 lsp 协议跟编辑器进行通信,而 analysis_server 接受插件请求,并且返回处理结果给插件。
Analysis Server API
Dart 插件 与 Analysis Server 的分为 2 种,Notifications 和 Requests 。官方文档地址
Analysis Server API Specification (htmlpreview.github.io)。
Notifications
当我们启动 analysis_server 的时候,我们首先将获得一个 server.connected 通知。
notification: {
"event": "server.connected"
"params": {
"version": String
"pid": int
}
}
Requests
当我们输入 {"id":"1","method":"server.getVersion"} 的时候,我们会得到一个返回 {"id":"1","result":{"version":"1.33.0"}},这里的 id 唯一的 uniqueId,以便对应请求和返回。
request: {
"id": String
"method": "server.getVersion"
}
response: {
"id": String
"error": optional RequestError
"result": {
"version": String
}
}
Domain
Analysis Server 包含了多个 domain,它们负责了不同的功能。
- analysis.getErrors
- analysis.getHover
- analysis.getLibraryDependencies
- analysis.getNavigation
- analysis.getReachableSources
- analysis.reanalyze
- analysis.setAnalysisRoots
- analysis.setGeneralSubscriptions
- analysis.setPriorityFiles
- analysis.setSubscriptions
- analysis.updateContent
- analysis.updateOptions
- completion.getSuggestions
- completion.getSuggestions2
- completion.setSubscriptions
- completion.registerLibraryPaths
- completion.getSuggestionDetails
- completion.getSuggestionDetails2
- search.findElementReferences
- search.findMemberDeclarations
- search.findMemberReferences
- search.findTopLevelDeclarations
- search.getTypeHierarchy
- edit.format
- edit.getAssists
- edit.getAvailableRefactorings
- edit.getFixes
- edit.getPostfixCompletion
- edit.getRefactoring
- edit.sortMembers
- edit.organizeDirectives
- execution.createContext
- execution.deleteContext
- execution.getSuggestions
- execution.mapUri
- execution.setSubscriptions
看完了这些,之前在终端里面操作应该就比较清楚了吧。我们也可以在 dart 命令行程序中启动 analysis_server。
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
Future<void> main(List<String> arguments) async {
var sdkPath = path.dirname(path.dirname(Platform.resolvedExecutable));
var vmPath = Platform.resolvedExecutable;
var scriptPath = '$sdkPath/bin/snapshots/analysis_server.dart.snapshot';
List<String> args = [scriptPath, '--sdk', sdkPath];
Process process = await Process.start(vmPath, args);
Stream<String> stream = process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.map((String message) {
print(message);
return message;
});
var broadcastStream = stream.asBroadcastStream();
await broadcastStream.first;
process.stdin.writeln('{"id":"1","method":"server.getVersion"}');
await broadcastStream.first;
}
这样的话,我们也可以利用 dart 命令行程序做一些骚操作了,比如阻止不规范的代码提交。
扩展提示以及导入
不知不觉前面就讲了这么多,终于到了正菜。虽然官方关于扩展方案提示导入的问题 Auto import (or quickfix?) for Extensions · Issue #38894 · dart-lang/sdk (github.com),已经关闭了,但是依然有很多问题。
当前行为
AndrodiStuido
能提示,能自动导入,但是提示会经常出问题,不同位置的代码一个能提示,一个却不能提示,就算是导入了引用也不提示。
VSCode
直接就是凉了,不能提示。只能手敲出完整的方法或者属性,利用快速修复导入引用。
另外,官方不会提示导包中的第2次导包。 例如 dartx 库里面 export 了 time, time 中的扩展不会提示。总结 AndrodiStuido 能用,但是经常抽风;VSCode 完全不能用。
改进后
利用 analyzer_plugin 的代码完成的功能,我对扩展方法的提示以及导入进行了一些优化。
AndrodiStuido
VSCode
总结,由于 VSCode 插件的问题, CompletionSuggestion not support auto import from library in vscode · Issue #4275 · Dart-Code/Dart-Code (github.com) 没法完成自动导入包的操作,只能根据提示,利用快速修复导入引用。AndrodiStuido 中表现良好(但不能保证插件作妖,在某些场景下,也会直接不显示,虽然已经返回的提示结果。)。为了保证性能,analyzer_plugin 只会对项目里面的文件或者引用了的库(除了在 pubspec.yaml 中,还需要在某个 dart 文件中 import )的相关文件进行解析,这就要求,如果你使用的是三方库,你至少需要在项目里面 import 过一次。
candies_analyzer_plugin
在实现扩展提示导入的过程中,主要参考了官方的 type_member_contributor.dart 和
extension_member_contributor.dart,对 Dart AST 有了更多的认识。
对 analyzer, analysis_server 和 analyzer_plugin 有了更多的理解。
原来的 candies_lints | Dart Package (flutter-io.cn) 已经重构迁移到 candies_analyzer_plugin | Dart Package (flutter-io.cn),自带了一些有用的 lint 以及支持扩展提示修复。如何使用该插件就不在这里重复讲了,请查看 Dart 怎么阻止你的同事使用Getx(自定义Lint) - 掘金 (juejin.cn)。调整之后的candies_analyzer_plugin 插件结构和官方更贴近一些,如果有其他方面的功能需求,到时候也可以添加。大家如果有好的想法,欢迎 pr 。
结语
对于不容易记住扩展全部方法名和属性的我来说,对扩展提示研究优化,应该能说会大大提供我工作时候的效率。很多时候,做一个工具做一个功能,还是因为想偷懒。
随便说下,在查找问题的过程中,我也看到了一些的言论。实际上在做开源的过程中,我也会遇到这种情况,一般来说我是保持平常心。虽然我也经常开玩笑说 辣鸡 dart ,辣鸡 flutter,但是还是希望我们对开源保持 respectful 和 courteous。
爱 Flutter,爱糖果,欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果
最最后放上 Flutter Candies 全家桶,真香。