theme: channing-cyan
本文正在参加「金石计划 . 瓜分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
刷新界面。
https://github.com/Dart-Code/Dart-Code/blob/3826e18056af5010cabfc59d25fc83a12e9db1f6/src/extension/analysis/analyzer.ts#L15
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
插件类似, 代码如下。
https://github.com/JetBrains/intellij-plugins/blob/f79dd6a1795aeef71569a726eb9215fab48a3f23/Dart/src/com/jetbrains/lang/dart/analyzer/DartAnalysisServerService.java#L2041
// 获取到 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小糖果QQ群:181398081
最最后放上 Flutter Candies 全家桶,真香。
暂无评论内容