Dart 扩展提示能用了吗?


theme: channing-cyan

本文正在参加「金石计划 . 瓜分6万现金大奖」

相关阅读:

前言

在上一篇 Dart 怎么阻止你的同事使用Getx(自定义Lint) – 掘金 (juejin.cn) 中,我们讲到了如何利用 analyzer_plugin 来自定义 Lint, 其实 analyzer_plugin 也能完成一些其他功能(当然我们对这些功能都觉得习以为常),我们一起看看这些的功能是如何完成的。

比如点击代码块,看到各种快捷方式。

截屏2022-11-21 13.21.54.png

这个大家应该经常使用到

截屏2022-11-21 13.25.30.png

代码完成建议,日常必使用的功能。

截屏2022-11-21 13.26.25.png

截屏2022-11-21 13.28.07.png

帮助识别类,字段,属性的全部关联,你可以通过 Change All Occurences 来对某个目标的全部关联进行修改。

截屏2022-11-21 13.52.24.png

截屏2022-11-21 13.41.21.png

截屏2022-11-21 13.29.29.png

截屏2022-11-21 13.30.14.png

这样看来,我们平时习以为常的功能,原来可以通过这些方法来实现,有点意思。那么 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.snapshotVSCode 的插件负责监听比如用户代码改变,发送消息给 analysis_serveranalysis_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,VSCodeanalysis_server 一定是以某种协议进行交互,是什么呢?

LSP

是的,没错,就是你们心里想的那个。输入法输入 lsp,评论区分享下你的是什么。

1669079976630.png

没错,就是 语言服务协议(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 种,NotificationsRequests 。官方文档地址
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,它们负责了不同的功能。

Server

Analysis

Completion

Search

Edit

Execution

Diagnostic

Flutter

看完了这些,之前在终端里面操作应该就比较清楚了吧。我们也可以在 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

能提示,能自动导入,但是提示会经常出问题,不同位置的代码一个能提示,一个却不能提示,就算是导入了引用也不提示。

默认as.gif

VSCode

直接就是凉了,不能提示。只能手敲出完整的方法或者属性,利用快速修复导入引用。
默认vs.gif

另外,官方不会提示导包中的第2次导包。 例如 dartx 库里面 exporttime, time 中的扩展不会提示。总结 AndrodiStuido 能用,但是经常抽风;VSCode 完全不能用。

改进后

利用 analyzer_plugin 的代码完成的功能,我对扩展方法的提示以及导入进行了一些优化。

AndrodiStuido

改进as.gif

VSCode

改进vs.gif

总结,由于 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 有了更多的认识。

analyzeranalysis_serveranalyzer_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,但是还是希望我们对开源保持 respectfulcourteous

截屏2022-11-21 19.58.03.png

Flutter,爱糖果,欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果QQ群:181398081

最最后放上 Flutter Candies 全家桶,真香。

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容