2011年,一位名叫Erich Gamma的瑞士计算机科学家加入微软。他最出名的是 “四人帮 “的作者之一,他们编写了颇具影响力的《设计模式》一书。在微软,伽马领导着一个位于瑞士苏黎世的小型开发实验室。2015年4月,该团队发布了Visual Studio Code的初始版本。我鼓励你观看第九频道(https://channel9.msdn.com/events/Build/2015/C9-05)对Erich Gamma的这个非常有趣的Visual Studio Code发布访谈。

Visual Studio Code是一个轻量级的多平台IDE(集成开发环境),它允许用户添加各种各样的扩展(在其他IDE中通常称为插件)。它是基于Electron开源框架的。

Visual Studio Code开始迅速流行起来,并在四年内成为开发者中最受欢迎的IDE。为什么它在这么短的时间内变得如此受欢迎?根据Visual Studio Code团队的说法,他们在开发的很早阶段就开始了狗粮。现在,100%的Visual Studio Code都是在Visual Studio Code中自己编写的。

“Dogfooding “是微软创造的一个术语,意思是在开发的早期阶段就使用你自己的产品。

但是你怎么能把它和它的 “老大哥 “微软Visual Studio相比呢?除了名字和相似的图标外,它们没有什么共同之处。首先,Visual Studio Code在所有主要平台上都是免费提供的。Windows、macOS和Linux。

与Visual Studio相比,Visual Studio Code的主要优势在于其可扩展性。它尤其包括能够将任何编程语言引入其中,为任何语言创建语法高亮,以及为任何语言创建调试器。

当然,Visual Studio Code并不能取代Visual Studio;仍有一些事情是Visual Studio在比Visual Studio Code更深的层次上做的。例如,你不能在Visual Studio Code中编写跨平台的Xamarin移动应用程序(至少在写这篇文章的时候不能)。如果你的目标是Windows GUI开发,你也会选择Visual Studio。但这在未来可能会改变。

Visual Studio Code的主要目的是支持开发者的生命周期(或者说,是一个循环)。编码(使用富语言服务) > 调试 > 检查你的代码(使用Git源代码控制集成)。

在这篇文章中,我将谈论添加一些富语言服务,关于REPL语言解释,以及关于调试。

作为添加到Visual Studio Code(为了简洁起见,我在本文中称之为VS Code)的语言的例子,我将使用CSCS(C#中的自定义脚本)。这是一种开源的轻型脚本语言,我在以前为CODE杂志写的文章中介绍过(https://www.codemag.com/article/1607081https://www.codemag.com/article/1711081)。

为Visual Studio代码增加语言支持

将富语言服务添加到IDE有几个层次。

  • 第一个层次是基本的语言支持(例如,语法高亮、括号匹配、自动缩进)。
  • 第二层是IntelliSense支持(代码完成,参数信息,内容辅助,以及其他一些)。
  • 第三层次是代码重构。

所有的富语言服务都由VS代码支持。

在这篇文章中,我将只介绍第一层。

你将在TypeScript语言中开发一个VS Code扩展。TypeScript是一种类型安全的JavaScript超集。你可以在这篇来自Sahil Malik(https://www.codemag.com/article/1511051)的CODE杂志的文章中阅读对它的介绍。

添加语法高亮和基本语言支持

下载并安装VS Code后,安装Node.js(https://nodejs.org/en/)。为了能够发布扩展,你还必须创建一个Visual Studio Team Services账户(编辑:VSTS现在是Azure DevOps)。注册后,你会收到发布者的名字,在下面生成扩展时,你会需要这个名字。

我将向你展示如何在第一层添加新的语言支持,包括。

  • 语法高亮
  • 括号匹配
  • 自动缩进

VS Code最酷的地方在于,你根本不需要任何编码来添加这些功能!所有的东西都可以通过配置来实现。一切都可以通过配置来实现。让我们开始吧。

转到命令行,首先通过运行Yeoman Extension Generator来安装。

  1. npm install -g yo generator-code

如果你是在macOS或Linux上,而你没有权限运行上述命令,可以尝试在上述命令前加上 “sudo”。

安装 “yo “工具后,你可以像这样运行扩展生成器。

  1. yo code

作为一种扩展类型,选择新的语言支持选项(见图1)。

用Visual Studio Code编写你自己的调试器和语言扩展 - 图1
图1:运行 “Yo “代码

然后回答与图2类似的问题。

image.png
图2:添加新的语言支持

对于发布者的名字,选择你在创建Visual Studio Team Services帐户时使用的名字。对于语言支持,Visual Studio Code使用TextMate语言语法(见https://manual.macromates.com/en/language_grammars)。定义语法的文件的扩展名是.tmLanguage。这种语法也被用于语法高亮。从头开始写这样一个文件是一个相当大的练习。因为从语法上讲,CSCS与C#相似,我只是取了一个定义C#语法的文件(https://raw.githubusercontent.com/dotnet/csharp-tmLanguage/master/grammars/csharp.tmLanguage),并在创建语言支持扩展时提供了它(见图2)。

一旦语言支持被创建,cs.tmLanguage文件将被复制到syntaxes子文件夹中。为了使它适用于CSCS,你必须在那里做一些改变。首先,将cs.tmLanguage文件名重命名为cscs.tmLanguage。然后在VS Code中打开这个文件,把出现的.cs替换成.cscs。

因为CSCS与C#不完全相同,所以还有一些其他的变化你应该做。例如,CSCS有elif关键字,这在C#中并不存在。要在语言语法中加入这个关键词,最简单的方法是找到else的做法,然后 “借用 “相应的代码即可。详见清单1。

清单1:对cscs.tmLanguage文件的补充

  1. <dict>
  2. <key>include</key>
  3. <string>#elif-part</string>
  4. </dict>
  5. <key>elif-part</key>
  6. <dict>
  7. <key>begin</key>
  8. <string>(?&lt;!\.)\b(elif)\b</string>
  9. <key>beginCaptures</key>
  10. <dict>
  11. <key>1</key>
  12. <dict>
  13. <key>name</key>
  14. <string>keyword.control.conditional.elif.cscs</string>
  15. </dict>
  16. </dict>
  17. <key>end</key>
  18. <string>(?&lt;=\})|(?=;)</string>
  19. <key>patterns</key>
  20. <array>
  21. <dict>
  22. <key>include</key>
  23. <string>#statement</string>
  24. </dict>
  25. </array>
  26. </dict>

你还需要在package.json文件中添加新的语言定义(这个文件是由扩展生成器生成的)。你把CSCS语言定义添加到package.json的贡献部分。

  1. "contributes": {
  2. "languages": [{
  3. "id": "cscs",
  4. "aliases": ["CSCS", "cscs"],
  5. "extensions": [".cscs"],
  6. "configuration": "./language-configuration.json"
  7. }],
  8. "grammars": [{
  9. "language": "cscs",
  10. "scopeName": "source.cscs",
  11. "path": "./syntaxes/cscs.tmLanguage"
  12. }],

language-configuration.json文件也是由扩展生成器生成的。基本上,它包含了开括号和闭括号、大括号、引号、注释等的定义。例如,这就是语言注释和括号的定义方式。

  1. "comments": {
  2. "lineComment": "//",
  3. "blockComment": [ "/*", "*/" ]
  4. },
  5. "brackets": [
  6. ["{", "}"],
  7. ["[", "]"],
  8. ["(", ")"]
  9. ],

如果你想引入一个新的注释或括号类型,language-configuration.json是用来做这个的文件。

这就是了! 仅仅是这几个配置上的改变,你就已经拥有了一个支持语法高亮的扩展了

如果你想看看新创建的扩展的运行情况,通过点击VS Code左侧面板上的调试图标切换到调试视图,然后按齿轮图标。一个新的VS Code实例被打开。这个新的实例现在已经知道了扩展名,新的语法高亮应该显示在所有扩展名为.cscs的文件上。

要查看新创建的扩展的运行情况,请点击VS Code左侧面板上的调试图标并按下齿轮图标。

发布一个只支持语法高亮的扩展可能没有什么意义,因为你还会开发一些其他的语言功能,你可以把语法高亮和其他功能一起包括进去。

添加一个REPL扩展

REPL(读取评估打印循环)是一种交互式计算机环境,允许评估简单的用户输入并返回结果。通常情况下,用户选择代码的某些部分并将其送去评估。如果没有选择任何内容,整行都会被送去评估。REPL最酷的地方是,你甚至可以选择一个被注释过的语句并将其送去进行评估

在本节中,你将为CSCS语言开发一个REPL扩展。你将看到它很容易调整为你选择的任何其他语言。

首先,你再次使用你在上一节中使用的哟扩展生成工具。这一次,你选择扩展类型New Extension(TypeScript)(这是默认的扩展类型)。然后,你把上一节中生成的语言文件(syntaxes\cscs.tmLanguage和language-configuration.json)复制到新的扩展文件夹中。现在你可以在VS Code中打开新生成的扩展文件夹。

第一个任务是创建一个新的命令,它将发送REPL请求进行评估。这些类型的任务大多位于package.json文件的contributes部分下。

让我们把REPL命令称为extension.cscs.rep(名字真的不重要),并给它分配一个键,在Windows和Linux上为Control + 9,在macOS上为Command ? 在macOS上则为Control +9。这可以在package.json文件的贡献部分完成,如下所示。

  1. "commands": [{
  2. "command": "extension.cscs.repl",
  3. "title": "CSCS REPL"
  4. }],
  5. "keybindings": [{
  6. "command": "extension.cscs.repl",
  7. "key": "ctrl+9",
  8. "mac": "cmd+9",
  9. "when": "editorTextFocus"
  10. }],

你还必须告诉VS Code何时激活该扩展。这在package.json的activationEvents部分完成。

  1. "activationEvents": ["onCommand:extension.cscs.repl"],

这意味着,只要用户按下Cmd+9/Ctrl+9键,该扩展就会被激活。

package.json文件的一个非常重要的部分是devDependencies部分,它包含了你的扩展所使用的所有包和它们的最小版本。

  1. "devDependencies": {
  2. "typescript": "^2.6.1",
  3. "vscode": "^1.1.6",
  4. "tslint": "^5.8.0",
  5. "@types/node": "^7.0.43",
  6. "@types/mocha": "^2.2.42"
  7. }

你在该部分添加新的包,在任何改变之后,你必须在项目文件夹目录下运行npm安装命令。

看一下清单2中package.json文件的全部内容。

清单 2: REPL项目package.json文件的一个片断

  1. {
  2. "name": "cscs-repl",
  3. "displayName": "CSCS REPL",
  4. "version": "0.1.4",
  5. "publisher": "vassilik",
  6. "engines": {
  7. "vscode": "^1.24.0"
  8. },
  9. "categories": ["Programming Languages","Other"],
  10. "keywords": ["CSCS", "repl", "scripting", "interpreter", "C#"],
  11. "activationEvents": ["onCommand:extension.cscs.repl"],
  12. "icon": "images/cscs128.png", "repository": {
  13. "type": "git",
  14. "url": "https://github.com/vassilych/cscs-repl.git"},
  15. "license": "MIT",
  16. "main": "./out/extension",
  17. "contributes": {
  18. "commands": [{
  19. "command": "extension.cscs.repl",
  20. "title": "CSCS REPL"
  21. }],
  22. "keybindings": [{
  23. "command": "extension.cscs.repl",
  24. "key": "ctrl+9",
  25. "mac": "cmd+9",
  26. "when": "editorTextFocus"
  27. }],
  28. "configuration": {
  29. "type": "object",
  30. "title": "CSCS REPL configuration",
  31. "properties": {
  32. "cscs.connectType": {
  33. "type": "string",
  34. "default": "sockets"
  35. },
  36. "cscs.serverPort": {
  37. "type": "number",
  38. "default": 13337
  39. },
  40. "cscs.serverHost": {
  41. "type": "string",
  42. "default": "127.0.0.1
  43. }
  44. }
  45. },
  46. "languages": [{
  47. "id": "cscs",
  48. "aliases": ["CSCS", "cscs"],
  49. "extensions": [".cscs"],
  50. "configuration": "./language-configuration.json"
  51. }],
  52. "grammars": [{
  53. "language": "cscs",
  54. "scopeName": "source.cscs",
  55. "path": "./syntaxes/cscs.tmLanguage"
  56. }]
  57. },
  58. "devDependencies": {
  59. "typescript": "^2.6.1",
  60. "vscode": "^1.1.6",
  61. "tslint": "^5.8.0",
  62. "@types/node": "^7.0.43",
  63. "@types/mocha": "^2.2.42"
  64. }
  65. }

你选择的扩展的编程语言是TypeScript。VS Code扩展的好处是,实际的功能实现可以用任何语言。扩展的TypeScript代码与VS Code框架进行通信,并从它那里接收用户命令。然后,它将这些命令发送给REPL评估器,后者将进行实际工作,并将结果反馈给扩展。

我决定通过套接字实现扩展和REPL评估器之间的通信。REPL评估器将在Visual Studio Xamarin中启动(正如我之前提到的,在VS Code中不支持运行和调试跨平台的移动应用程序,至少现在不支持)。

REPL评估器的实现

为了收集来自REPL客户端的所有请求,使用一个队列,以System.Collections.Concurrent命名空间的BlockingCollection类来实现。这个类的主要优点是,它提供了一种线程安全的方式,将项目排队到指定的队列中,并从那里排队,这样我们就不必专注于这些低层次的细节。

REPL评估器服务器必须支持不同的情况。

  • 如果服务器正在运行一个Xamarin移动应用程序,代码必须在主GUI线程上进行评估。具体做法如下。

在iOS系统中:

  1. UIApplication.SharedApplication.InvokeOnMainThread(() => {
  2. // This code is run on the main thread.
  3. });

在Android系统中:

  1. Activity.RunOnUiThread(()
  2. => {
  3. // This code is run on the main thread.
  4. });
  • 如果评估器运行在Unity上,就不需要在主线程上安排运行代码,因为Unity每一帧都在主线程上调用Update()方法。在Update()结束时,你可以处理所有排队的请求。

  • 如果是一个控制台应用程序,或者当评估器不需要在主线程上运行时,你可以在同一个线程上处理所有的请求。

在Xamarin移动应用程序的情况下,代码必须在主GUI线程上进行评估。

我实现了上述三种情况。显然,还有很多需要在主线程上运行代码的场景,你可能想加入。例如,如果你想在Windows Phone主线程上评估代码,你可以按以下方式安排调用。

  1. CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.High, () => {
  2. // This code is run on the main thread.
  3. });

DebuggerServer类在清单3中实现。我很快就会审查它。

清单3:DebuggerServer C#类的一个片段

  1. public class DebuggerServer
  2. {
  3. public static Action<Debugger, string> OnRequest;
  4. public static bool DebuggerAttached { set; get; }
  5. static Debugger m_debugger
  6. static TcpClient m_client;
  7. static NetworkStream m_stream;
  8. static BlockingCollection<string> m_queue = new BlockingCollection<string>();
  9. public static void StartServer(int port = 13337)
  10. {
  11. ThreadPool.QueueUserWorkItem(StartServerBlocked, port);
  12. }
  13. public static void StartServerBlocked(Object threadContext)
  14. {
  15. int port = (int)threadContext;
  16. IPAddress localAddr = IPAddress.Parse("127.0.0.1");
  17. TcpListener server = new TcpListener(localAddr, port);
  18. server.Start();
  19. DebuggerAttached = true;
  20. while (true) {
  21. // Perform a blocking call to accept requests.
  22. m_client = server.AcceptTcpClient();
  23. m_stream = m_client.GetStream();
  24. ThreadPool.QueueUserWorkItem(RunClient);
  25. }
  26. }
  27. static void RunClient()
  28. {
  29. Byte[] bytes = new Byte[1024];
  30. string data = null;
  31. int i;
  32. ThreadPool.QueueUserWorkItem(StartProcessing, null);
  33. m_debugger = new Debugger();
  34. Debugger.OnResult += SendBack;
  35. try {
  36. while((i = m_stream.Read(bytes, 0, bytes.Length)) != 0) {
  37. data = System.Text.Encoding.UTF8.GetString(bytes, 0, i);
  38. m_queue.Add(data);
  39. }
  40. } catch(Exception exc) {
  41. Console.Write ("Client disconnected: {0}", exc.Message);
  42. }
  43. Debugger.OnResult -= SendBack;
  44. m_client.Close();
  45. }
  46. static void StartProcessing(Object threadContext)
  47. {
  48. #if UNITY_EDITOR
  49. // ProcessQueue will be called directly from Unity Update()
  50. # else
  51. try {
  52. ProcessQueue();
  53. } catch(Exception exc) {
  54. Console.Write("Connection is over: {0}", exc.Message);
  55. }
  56. #endif
  57. }
  58. public static void ProcessQueue()
  59. {
  60. string data;
  61. #if UNITY_EDITOR
  62. while(m_queue.TryTake(out data)) {
  63. #else
  64. while (true)
  65. {
  66. // A blocking call
  67. data = m_queue.Take();
  68. #endif
  69. if (OnRequest != null) {
  70. OnRequest?.Invoke(m_debugger, data);
  71. } else {
  72. m_debugger.ProcessClientCommands(data);
  73. }
  74. }
  75. }
  76. static void SendBack(string str)
  77. {
  78. byte[] msg = System.Text.Encoding.UTF8.GetBytes(str);
  79. try {
  80. m_stream.Write(msg, 0, msg.Length);
  81. m_stream.Flush();
  82. } catch(Exception exc) {
  83. Console.Write("Client disconnected: {0}", exc.Message);
  84. return;
  85. }
  86. }
  87. }

服务器通过调用StartServer()异步启动,或者以StartServerBlocked()阻塞调用的方式启动。服务器在配置的端口上初始化服务器套接字(默认为13337),并开始监听进入的连接。

  1. IPAddress localAddr = IPAddress.Parse("127.0.0.1");
  2. TcpListener server = new TcpListener(localAddr, port);
  3. server.Start();
  4. DebuggerAttached = true;
  5. while (true) {
  6. m_client = server.AcceptTcpClient();
  7. m_stream = m_client.GetStream();
  8. ThreadPool.QueueUserWorkItem(RunClient);
  9. }

对于每个被接受的连接,服务器运行RunClient()方法,它将在一个新的池线程中启动。

一方面,RunClient()方法开始监听客户端的请求,将它们全部加入BlockingCollection m_queue。

  1. while((i = m_stream.Read(bytes, 0, bytes.Length)) != 0) {
  2. data = Encoding.UTF8.GetString(bytes, 0, i);
  3. m_queue.Add(data);
  4. }

另一方面,它启动另一个线程,在ProcessQueue()方法中消耗这个队列。这个方法可以直接从Unity中调用;如果队列是空的,它不会做任何事情,否则它会处理队列。在Unity的情况下,OnRequest事件处理程序是空的,所以这是在主线程上调用的Unity Update()方法中执行的代码。

  1. while(m_queue.TryTake(out data)) {
  2. m_debugger.ProcessClientCommands(data);
  3. }

ProcessClientCommands()方法是进行实际处理的地方;你将在后面看到它。在其他情况下,你会执行对队列m_queue.Take()的调用,它会阻塞直到队列不空。

  1. while(true) { // A blocking call.
  2. data = m_queue.Take();
  3. if (OnRequest != null) { // iOS/Android case
  4. OnRequest?.Invoke(m_debugger, data);
  5. } else { // Direct call case
  6. m_debugger.ProcessClientCommands(data);
  7. }
  8. }

在Xamarin(iOS/Android应用程序)的情况下,一个事件处理程序被触发了。该事件处理程序确保处理是在主线程上完成的。在其他情况下,你在同一个线程上开始处理队列(在控制台应用程序中,你可能并不关心在哪个线程上进行处理)。

现在让我们看看如何让CSCS Xamarin应用程序知道它们必须知道客户端在主线程上等待REPL的评估。你首先向CSCS解析器注册一个CSCS函数。

  1. ParserFunction.RegisterFunction("StartDebugger", new DebuggerFunction());

这意味着调用CSCS函数StartDebugger在幕后调用DebuggerFunction.Evaluate()方法,如清单4所示。

清单4:DebuggerFunction C#类的实现

  1. class DebuggerFunction : ParserFunction
  2. { protected override Variable Evaluate(ParsingScript script)
  3. {
  4. List<Variable> args = script.GetFunctionArgs();
  5. int port = Utils.GetSafeInt(args, 0, 13337);
  6. DebuggerServer.StartServer(port);
  7. DebuggerServer.OnRequest += ProcessRequest;
  8. return Variable.EmptyInstance;
  9. }
  10. public void ProcessRequest(Debugger debugger, string request)
  11. {
  12. #if __ANDROID__
  13. MainActivity.TheView.RunOnUiThread(() => {
  14. debugger.ProcessClientCommands(request);
  15. });
  16. #elif __IOS__
  17. UIApplication.SharedApplication.InvokeOnMainThread(() => {
  18. debugger.ProcessClientCommands(request);
  19. });
  20. Else
  21. debugger.ProcessClientCommands(request);
  22. #endif
  23. }
  24. }

该方法启动REPL服务器并订阅传入的客户端请求。

  1. DebuggerServer.StartServer(port);
  2. DebuggerServer.OnRequest += ProcessRequest;.

当ProcessRequest()方法被触发时,它确保每个请求在主线程上被处理。

  1. public void ProcessRequest(Debugger debugger, string request) {
  2. #if __ANDROID__
  3. MainActivity.TheView.RunOnUiThread(() => {
  4. debugger.ProcessClientCommands(request);
  5. });
  6. #elif __IOS__
  7. UIApplication.SharedApplication.InvokeOnMainThread( () => {
  8. debugger.ProcessClientCommands(request);
  9. });
  10. }

在ProcessRequest()方法中,你可能想添加你需要的其他场景(Windows Phone、WPF等)。

Unity是一个特殊的例子。因为Update()方法是在主线程上调用的,所以只要在Update()方法的末尾放上以下语句就可以了(见清单3中ProcessQueue()方法的实现)。

  1. if (DebuggerServer.DebuggerAttached) {
  2. DebuggerServer.ProcessQueue();
  3. }

Unity是一个特殊情况,因为Update()方法已经在主线程上被调用了。

现在是时候看看实际处理了。因为服务器可能同时收到来自客户端的几个请求(它们可以被排队,正如你以后在TypeScript中开发客户端时看到的),你必须一个一个地处理这些请求。这里的假设是,一个请求不包含一个新行—你将确保客户端会处理这个问题。

请注意,在数据请求中没有新行不会有问题,因为新行在CSCS中不扮演任何角色,与Python不同。在CSCS中,表达式由分号分隔,就像在C#中一样。

  1. public void ProcessClientCommands(string data)
  2. {
  3. string [] commands = data.Split(new char [] {'\n'});
  4. foreach (string dataCmd in commands) {
  5. if (!string.IsNullOrWhiteSpace(dataCmd)) {
  6. ProcessClientCommand(dataCmd);
  7. }
  8. }
  9. }

这是你如何处理每个请求的一个片段(片段是因为你在开发调试器时要扩展这个方法)。

  1. void ProcessClientCommand(string data) {
  2. string [] parts = data.Split(new char [] {'|'});
  3. string cmd = parts[0].ToLower();
  4. string result = "N/A";
  5. if (cmd == "repl") {
  6. result = ProcessRepl(data.Substring(cmd.Length+1));
  7. SendBack(result);
  8. return;
  9. }
  10. }

正如你所看到的,服务器期望来自客户端的请求是以下格式:命令|请求数据。特别是,对于REPL来说,它期望repl|string来处理。

SendBack()方法是一个较低级别的函数,用于发送回结果。

  1. static void SendBack(string str) {
  2. byte [] msg = Encoding.UTF8.GetBytes(str);
  3. try {
  4. m_stream.Write(msg, 0, msg.Length);
  5. m_stream.Flush();
  6. } catch (Exception exc) {
  7. Console.Write("Client gone: {0}", exc.Message);
  8. return;
  9. }
  10. }

ProcessRepl()方法进行实际的CSCS处理。它的实现在清单5中显示。在这里,你可以定制REPL处理,并使用你选择的任何其他语言。

清单5:ProcessRepl() C#方法

  1. string ProcessRepl(string repl)
  2. {
  3. Dictionary<int, int> char2Line;
  4. string script = Utils.ConvertToScript(repl, out char2Line);
  5. ParsingScript tempScript = new ParsingScript(script, 0);
  6. tempScript.Debugger = this;
  7. Variable result = null;
  8. try {
  9. while (tempScript.Pointer < script.Length) {
  10. result = tempScript.ExecuteTo();
  11. tempScript.GoToNextStatement();
  12. }
  13. } catch (Exception exc) {
  14. return "Exception thrown: " + exc.Message;
  15. }
  16. string stringRes = Output + "\n";
  17. stringRes += result == null ? "" : result.AsString();
  18. return stringRes;
  19. }

REPL客户端的实现

你将使用TypeScript实现REPL客户端(当你使用yo工具创建扩展时,扩展语言只有两个选项:JavaScript和TypeScript)。

VS Code扩展的入口是extension.ts文件的activation()方法。它的实现在清单6中显示。

清单6:extension.ts的activate()方法

  1. import * as vscode from 'vscode';
  2. import { CscsRepl } from './cscsRepl';
  3. export function activate(context: vscode.ExtensionContext) {
  4. const config = vscode.workspace.getConfiguration('cscs');
  5. let connectTypeConfig = config.get('connectType');
  6. let hostConfig = config.get('serverHost');
  7. let portConfig = config.get('serverPort');
  8. let connectType = connectTypeConfig ? connectTypeConfig.toString() : '';
  9. let host = hostConfig ? hostConfig.toString() : '';
  10. let port = portConfig ? parseInt(portConfig.toString()) : 0;
  11. let cscsRepl = new CscsRepl();
  12. cscsRepl.start(connectType, host, port);
  13. let outputChannel = vscode.window.createOutputChannel('CSCS');
  14. cscsRepl.on('onInfoMessage', (msg : string) => {
  15. vscode.window.showInformationMessage('REPL: ' + msg);
  16. });
  17. cscsRepl.on('onReplMessage', (data : string) => {
  18. outputChannel.append('REPL> ');
  19. let lines = data.toString().split('\n');
  20. let counter = 0;
  21. for (let i = 0; i < lines.length; i++) {
  22. let line = lines[i].trim();
  23. outputChannel.appendLine(line);
  24. counter++;
  25. }
  26. });
  27. const getCode = () => {
  28. let textEditor = vscode.window.activeTextEditor;
  29. if (!textEditor) {
  30. return "";
  31. }
  32. let selection = textEditor.selection;
  33. let text = textEditor.document.getText(selection);
  34. if (textEditor.selection.start.line === textEditor.selection.end.line &&
  35. textEditor.selection.start.character === textEditor.selection.end.character) {
  36. text = textEditor.document.lineAt(textEditor.selection.start.line).text;
  37. }
  38. return text;
  39. };
  40. let disposable = vscode.commands.registerCommand('extension.cscs.repl', () => {
  41. let code = getCode();
  42. if (code === '') {
  43. return;
  44. }
  45. cscsRepl.sendToServer('repl', code);
  46. });
  47. context.subscriptions.push(disposable);
  48. }

首先,它读取你在package.json文件中定义的配置,即。

  1. "properties": {
  2. "cscs.connectType": {
  3. "type": "string",
  4. "default": "sockets"
  5. },
  6. "cscs.serverPort": {
  7. "type": "number",
  8. "default": 13337
  9. },
  10. "cscs.serverHost": {
  11. "type": "string",
  12. "default": "127.0.0.1"
  13. }
  14. }

读取配置后,你连接到你在上一节实现的调试器。通过套接字实现连接的代码在CscsRepl类中,在cscsRepl.ts中实现。它显示在清单7中。

清单7:在Typescript中连接到调试器

  1. const Net = require("net");
  2. private _debugger = new Net.Socket();
  3. private _queuedCommands = new Array<string>();
  4. public start(connectType: string, host: string, port: number) {
  5. if (this._init) {
  6. return;
  7. }
  8. this._init = true;
  9. this._connectType = connectType;
  10. this._host = host;
  11. this._port = port;
  12. this.connectToDebugger();
  13. }
  14. public connectToDebugger() : void {
  15. this._connected = false;
  16. if (this._connectType === "sockets") {
  17. console.log('Connecting to ' + this._port + " on " + this._host + "...");
  18. this._debugger.connect(this._port, this._host, () => {
  19. this._connected = true;
  20. this.printInfoMsg('Connected to the server at ' + this._host + ":" + this._port);
  21. for (let i = 0; i < this._queuedCommands.length; i++) {
  22. this.sendToServer(this._queuedCommands[i]);
  23. }
  24. this._queuedCommands.length = 0;
  25. });
  26. this._debugger.on('data', (data : string) => {
  27. this.processFromDebugger(data.toString().trim());
  28. });
  29. this._debugger.on('close', () => {
  30. if (!this._connected) {
  31. this.printErrorMsg("Couldn't connect to " + this._host + ":" + this._port);
  32. }
  33. this._connected = false;
  34. });
  35. }
  36. }

请注意,你订阅了来自CscsRepl的信息,并使用此代码像弹出窗口一样显示它们。

  1. cscsRepl.on('onInfoMessage', (msg : string) => {
  2. vscode.window.showInformationMessage('REPL: ' + msg);
  3. });

在CscsRepl中,你像这样把信息传递给订阅者。

  1. public printInfoMsg(msg : string) {
  2. this.sendEvent('onInfoMessage', msg);
  3. }
  4. protected processFromDebugger(data : string) {
  5. this.sendEvent('onReplMessage', msg);
  6. }

调试器的实际输出显示在你在本语句中创建的名为CSCS的额外输出窗口中。

  1. let outputChannel = vscode.window.createOutputChannel('CSCS');

extension.ts activate()方法的主要部分是extension.cscs.rep命令的实现,你之前在package.json中定义了这个命令。

  1. let disposable=vscode.commands.registerCommand('extension.cscs.repl', () => {
  2. let code = getCode();
  3. if (code === '') {
  4. return;
  5. }
  6. cscsRepl.sendToServer('repl', code);
  7. });

每次用户执行extension.cscs.rep命令时,你会得到她在编辑器中选择的内容(使用getCode()方法,如清单8所示),然后将她的选择发送到服务器(见清单9)。所有来自服务器的回复都显示在cscsRepl.on(‘onReplMessage’)方法中,也在清单8中显示。

清单 8: 在 Typescript 中将 REPL 发送给调试器

  1. public sendToServer(cmd : string, data = "") {
  2. if (this._finished) {
  3. return;
  4. }
  5. let lines = data.split('\n');
  6. let load = "";
  7. for (let i = 0; i < lines.length; i++) {
  8. let lineData = lines[i].trim();
  9. if (lineData === "") {
  10. continue;
  11. }
  12. let lines2 = lineData.split('\r');
  13. load = load + lines2[0];
  14. }
  15. if (load !== "" || !cmd.includes('|')) {
  16. cmd = cmd + "|" + load;
  17. }
  18. if (!this._connected) {
  19. this._queuedCommands.push(cmd);
  20. return;
  21. }
  22. this._debugger.write(cmd + "\n");
  23. }

清单9:CSCS调试器项目package.json文件的一个片段

  1. "name": "cscs-debugger",
  2. "displayName": "CSCS Debugger",
  3. "activationEvents": ["onDebug"],
  4. "contributes": {
  5. "languages": [{
  6. "id": "cscs",
  7. "aliases": ["CSCS", "cscs"],
  8. "extensions": [".cscs"],
  9. "configuration": "./language-configuration.json"
  10. }],
  11. "breakpoints": [{"language": "cscs"}],
  12. "debuggers": [{
  13. "type": "cscs",
  14. "label": "CSCS Debug",
  15. "program": "./out/debugAdapter.js",
  16. "runtime": "node",
  17. "configurationAttributes": {
  18. "launch": { "required": ["program"] },
  19. "properties": {
  20. "program": {
  21. "type": "string",
  22. "description": "Absolute path to a text file.",
  23. "default": "${workspaceFolder}/${command:AskForProgramName}"
  24. },
  25. "serverPort": {
  26. "type": "number",
  27. "description": "Number of the server port to connect to.",
  28. "default": 13337
  29. },
  30. }
  31. }
  32. }

就这样吧! 你看到了客户端和服务器的实现,现在是时候看看REPL在现实生活中是如何工作的。

REPL客户端的操作

让我们看看如何在iOS调试中使用REPL评估器。从https://github.com/vassilych/mobile 下载CSCS Xamarin项目。VS Code CSCS REPL扩展可以从https://marketplace.visualstudio.com/items?itemName=vassilik.cscs-repl

start.cscs脚本是开始处理CSCS的地方。你可以在这里只放一条语句。

  1. StartDebugger();

这条语句将开始监听默认端口13337上的传入连接(你可以提供任何其他端口号作为StartDebugger()函数的参数)。然后开始在调试模式下运行Xamarin项目,选择一个模拟器或一个真实设备。Xamarin用一个空屏幕启动一个iOS应用程序。

使用REPL VS Code扩展,你可以在飞行中向iOS应用程序添加小部件。

现在你可以使用VS Code中的REPL评估器,开始向iOS应用中添加小部件。一些例子见图3。你还可以删除小部件,改变标签,引入新的动作,等等。使用 REPL 评估器是获得你所需要的布局的最快方式,而无需重新启动调试器。这不是很酷吗?

用Visual Studio Code编写你自己的调试器和语言扩展 - 图3
图3:在iPhone上玩REPL。

在Visual Studio代码中添加调试器

在VS Code中实现一个调试器比实现一个REPL解释器要复杂一些。我建议你阅读微软的介绍(https://code.visualstudio.com/docs/extensions/example-debuggers),关于为模拟调试器写一个VS Code扩展。那里的模拟调试器模仿的是与真正的调试器的连接。在这里你将看到调试器扩展连接到真正的调试器服务器。

首先,用之前的方法创建一个新的扩展,使用yo命令行工具。

你还需要包括更多的依赖项来实现调试器,特别是两个npm模块:vscode-debugprotocol和vscode-debugadapter。见清单9中package.json文件的内容。注意,在做了与你的扩展所使用的npm模块有关的任何修改后,你需要运行npm安装命令,它将相应的模块下载到你的项目目录下的node_modules文件夹中。

VS Code Debugger项目的结构与普通的扩展不同,比如你在上一节开发的REPL项目。

首先,主要的动作并不是由扩展的激活方法触发的。调试器本身作为一个独立的程序运行,即所谓的调试适配器。为了实现一个调试器,你必须实现调试适配器协议。你将在接下来的章节中看到如何做。

你要实现的调试器可以执行以下调试器任务。

  • 设置和取消断点
  • 下一步,继续,步入,和步出命令
  • 保持对局部和全局变量的跟踪
  • 追踪被观察的变量
  • 保持对调用栈的跟踪
  • 通过将鼠标悬停在变量上显示变量值
  • 通过将鼠标悬停在函数名称上显示函数定义
  • 处理代码异常情况

调试器客户端

在客户端,CscsDebugSession是主要的调试器类。就像在REPL的情况下,所有的客户端代码都是TypeScript的。它扩展并实现了LoginDebugSession,它扩展了DebugSession类。请看清单10中的实现片段。你可以在GitHub上看到完整的实现(链接在侧边栏提供)。

清单 10:CscsDebugger TypeScript 类的片段

  1. export class CscsDebugSession extends LoggingDebugSession {
  2. private static THREAD_ID = 1;
  3. private _runtime: CscsRuntime;
  4. public constructor() {
  5. super("cscs-debug.txt");
  6. this.setDebuggerLinesStartAt1(false);
  7. this.setDebuggerColumnsStartAt1(false);
  8. this._runtime = new CscsRuntime();
  9. this._runtime.on('stopOnStep', () => {
  10. this.sendEvent(new StoppedEvent('step', CscsDebugSession.THREAD_ID));
  11. });
  12. this._runtime.on('stopOnBreakpoint', () => {
  13. this.sendEvent(new StoppedEvent('breakpoint', CscsDebugSession.THREAD_ID));
  14. });
  15. this._runtime.on('output', (text, filePath, line, column) => {
  16. const e: DebugProtocol.OutputEvent = new OutputEvent(`${text}\n`);
  17. e.body.source = this.createSource(filePath);
  18. e.body.line = this.convertDebuggerLineToClient(line);
  19. this.sendEvent(e);
  20. });
  21. }
  22. protected initializeRequest(response: DebugProtocol.InitializeResponse,
  23. args: DebugProtocol.InitializeRequestArguments): void {
  24. response.body = response.body || {};
  25. response.body.supportsEvaluateForHovers = true;
  26. response.body.supportsStepBack = false;
  27. response.body.supportsSetVariable = false;
  28. response.body.supportsRestartRequest = false;
  29. response.body.supportsModulesRequest = false;
  30. this.sendResponse(response);
  31. this.sendEvent(new InitializedEvent());
  32. }
  33. protected continueRequest(response: DebugProtocol.ContinueResponse,
  34. args: DebugProtocol.ContinueArguments): void {
  35. this._runtime.continue();
  36. this.sendResponse(response);
  37. }
  38. protected nextRequest(response: DebugProtocol.NextResponse,
  39. args: DebugProtocol.NextArguments): void {
  40. this._runtime.step();
  41. this.sendResponse(response);
  42. }
  43. protected evaluateRequest(response: DebugProtocol.EvaluateResponse,
  44. args: DebugProtocol.EvaluateArguments): void {
  45. let reply: string | undefined = undefined;
  46. if (args.context === 'hover') {
  47. reply = this._runtime.getHoverValue(args.expression);
  48. } else if (args.context === 'watch') {
  49. reply = this._runtime.getVariableValue(args.expression);
  50. }
  51. response.body = { result: reply ? reply : "", variablesReference: 0
  52. };
  53. this.sendResponse(response);
  54. }
  55. }

CscsDebugSession在debugAdapter.ts文件中启动,如下所示。

  1. import { CscsDebugSession } from './cscsDebug';
  2. CscsDebugSession.run(CscsDebugSession);

在initializeRequest方法中,你可以设置调试器的一些功能。下面是我设置的一些选项。

  1. response.body.supportsEvaluateForHovers = true;
  2. response.body.supportsSetVariable = false;
  3. response.body.supportsRestartRequest = false;
  4. response.body.supportsRestartFrame = false;
  5. response.body.supportsModulesRequest = false;

你可以在GitHub上的VS Code Debug Protocol的Capabilities部分看到所有可用的选项。(https://github.com/Microsoft/vscode/blob/master/src/vs/workbench/parts/debug/common/debugProtocol.d.ts))。

CscsDebuggerSession使用CscsRuntime来完成连接到调试服务器的实际工作,并将大部分请求委托给它。在附带的源代码下载中查看其实现。该代码与你在清单7中实现REPL评估器扩展时看到的非常相似。

CscsDebuggerSession构造函数订阅由CscsRuntime处理的事件,并将它们传播给DebugSession。

  1. this._runtime.on('stopOnStep', () => {
  2. this.sendEvent(new StoppedEvent('step', CscsDebugSession.THREAD_ID));
  3. });
  4. this._runtime.on('stopOnBreakpoint', () => {this.sendEvent(new StoppedEvent('breakpoint',
  5. CscsDebugSession.THREAD_ID));
  6. });
  7. this._runtime.on('end', () => {
  8. this.sendEvent(new TerminatedEvent());
  9. });

connectToDebugger()方法与你之前在清单7中看到的REPL扩展中的同名方法相似,但有一些区别。首先,当你连接到调试服务器时,你要发送你要调试的初始文件。这是在这个语句中完成的。

  1. this.sendToServer("file", this._sourceFile);

你可以假设你和调试服务器在同一台电脑上,只发送要调试的文件的路径。

第二,你把所有的断点发送到服务器上。

正如你在所附的源代码中看到的,CscsDebuggerSession类基本上是CscsRuntime类的一个包装。另一方面,CscsRuntime类将大部分任务委托给调试服务器,通过套接字与它进行通信。

添加和删除断点

一旦用户打开了要调试的文件,只要用户点击了编辑器的左侧,VS代码框架就会调用CscsDebug类中的setBreakpointRequest()方法。这个方法在两种情况下都被调用:当一个新的断点被添加或删除时。没有任何迹象表明是什么触发了对该方法的调用。相反,SetBreakPointsArguments参数包含了一个特定文件的所有断点。因此,你只需删除指定文件的所有现有断点,然后再一个一个地添加它们。下面是其中的代码片段。

  1. protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse,
  2. args: DebugProtocol.SetBreakpointsArguments): void
  3. {
  4. const path = <string>args.source.path;
  5. const clientLines = args.lines || [];
  6. this._runtime.clearBreakpoints(path);
  7. const actualBreakpoints = clientLines.map(l =>
  8. {
  9. let { verified, line, id } = this._runtime.setBreakPoint(path,
  10. this.convertClientLineToDebugger(l));

断点的注册将被委托给CscsRuntime类中的setBreakPoint()方法。在这里,你可以创建一个从源代码的行号到断点的映射。以后当你需要知道某一行是否包含断点时,就需要这样做。

下一步,步入,步出,继续

接下来,Step-in、Step-out 和 Continue 可以说是最常见的调试操作。当用户触发这四项中的任何一项时,相应的函数会在CscsDebug类上触发,因为它扩展了DebugSession类。例如,在下一个请求的情况下,这个方法会在CscsDebug中执行。

  1. protected nextRequest(response:
  2. DebugProtocol.NextResponse, args:
  3. DebugProtocol.NextArguments): void {
  4. this._runtime.step();
  5. this.sendResponse(response);
  6. }

基本上,所有的工作都委托给了CscsRuntime。

  1. public step(event = 'stopOnStep') {
  2. this._continue = false;
  3. if (this._init) {
  4. this.runOnce(event);
  5. } else {
  6. this.sendToServer('next');
  7. }
  8. }

sendToServer方法与REPL客户端的sendToServer方法相同(见清单8)。runOnce()方法总是在程序开始时执行(意味着你总是在要调试的文件的第一行停止)。

runOnce() 方法调用 fireEventsForLine() 方法。fireEventsForLine()方法通知debugSession接下来要做什么。有三种情况。

  • 你已经碰到了一个断点。然后你验证它,如果尚未验证的话,并通知debugSession你应该在这里停止。
  1. let bp = this.getBreakPoint(ln);
  2. if (bp) {
  3. this.sendEvent('stopOnBreakpoint');
  4. if (!bp.verified) {
  5. bp.verified = true;
  6. this.sendEvent('breakpointValidated', bp);
  7. }
  8. return true;
  9. }
  • 你停在那里是因为你处于步骤事件模式,而且这一行不是空的(该事件是 “stopOnStep”)。
  1. if (stepEvent && line.length > 0) {
  2. this.sendEvent(stepEvent);
  3. return true;
  4. }

否则就继续执行。在附带的源代码下载中查看完整的实现。

局部和全局变量

每次向调试服务器发送请求时(见清单8),在processFromDebugger()方法中异步等待回复。与回复一起,你等待的是关于所有当前局部和全局变量的信息。你也期望在每个回复中收到当前的调用堆栈信息。

在fillVars()方法中加载所有的变量,这发生在这个语句中。

  1. if (globLoc === '1') {
  2. this._globalVariables.push(item);
  3. } else {
  4. this._localVariables.push(item);
  5. }

CscsDebugger会定期向CscsRuntime询问全局变量和局部变量。

  1. protected variablesRequest(response:
  2. DebugProtocol.VariablesResponse, args:
  3. DebugProtocol.VariablesArguments): void {
  4. let variables = args.variablesReference === this._localScope ?
  5. this._runtime.localVariables :
  6. this._runtime.globalVariables
  7. response.body = { variables: variables };
  8. this.sendResponse(response);
  9. }

variablesRequest()方法被调用两次:一次用于局部变量,一次用于全局变量。无论你返回什么,都会显示在VS代码的左侧编辑窗格中。见图4。

用Visual Studio Code编写你自己的调试器和语言扩展 - 图4
图4:VS代码调试器的运行情况

观察和悬停变量

在watch中添加变量完全是由VS Code Framework完成的。将鼠标悬停在一个变量上也是一样的,但是当VS代码框架问你时,你仍然要告诉watch和hover这两个变量的值是什么。

这是在DebugSession evaluateRequest()方法中完成的。这是CscsDebugger如何扩展这个方法的一个片段(完整代码见清单10)。

  1. protected evaluateRequest(): void {
  2. let reply: string | undefined = undefined;
  3. if (args.context === 'hover') {
  4. reply = this._runtime.getHoverValue(args.expression);
  5. } else if (args.context === 'watch') {
  6. reply = this._runtime.getVariableValue(args.expression);
  7. }
  8. this.sendResponse(response);
  9. }

基本上,悬停和观察功能的评估被委托给了CscsRuntime类。在那里,它查找之前在调试服务器回复上存储的变量值。

  1. public getVariableValue(key : string) : string {
  2. let val = this._variablesMap.get(key);
  3. if (val) {
  4. return val;
  5. }
  6. return "--- unknown ---";
  7. }

对于悬停请求,代码是类似的。

  1. public getHoverValue(key : string) : string {
  2. let hover = this._hoversMap.get(key);
  3. if (hover) {
  4. return hover;
  5. }
  6. hover = this._functionsMap.get(key);
  7. if (hover) {
  8. return hover;
  9. }
  10. return key;
  11. }

_variablesMap和_hoversMap变量是在fillVars()方法中填写的。_functionsMap变量是一个特殊的静态地图,有所有的自定义语言函数定义。它可以被实现并从服务器接收,但我决定在TypeScript客户端代码中硬编码它更容易。详见清单11中的initFunctionNames()方法。

清单11:initFunctionNames()TypeScript方法的一个片段

  1. private initFunctionNames() : void {
  2. let ifelse = "if(condition) { ... } elif (condition) {} else {}:
  3. if-elif-else control flow. Curly braces {} are mandatory!";
  4. this._functionsMap.set("if", ifelse);
  5. this._functionsMap.set("elif", ifelse);
  6. this._functionsMap.set("else", ifelse);
  7. this._functionsMap.set("while", "while(condition) { ... }:
  8. While control flow. Curly braces {} are mandatory!");
  9. this._functionsMap.set("for", "for(i : array) OR for(i=0; i<n;
  10. i++) { ... }: For control flow statements. Curly braces {} are mandatory!");
  11. this._functionsMap.set("function", "function f(arg1, arg2, ...)
  12. { ... } : CSCS custom interpreted function (use cfunction for pre-compiled functions)");
  13. this._functionsMap.set("print", "print(arg1, arg2, ...): Prints passed arguments to console");
  14. this._functionsMap.set("type", "type(arg): Returns type of the passed arg");
  15. this._functionsMap.set("include", "include(filename): includes CSCS code from the filename");
  16. this._functionsMap.set("sin", "sin(x): Returns sine of x");
  17. }

栈跟踪请求

与来自调试服务器的每个回复一起,你也希望收到本地和全局变量以及当前的调用堆栈信息。你在fillStackTrace()方法中填充堆栈。

保存堆栈跟踪信息的对象是_stackTrace。它是一个StackEntry对象的数组。每个StackEntry由文件名、行号和名称组成,也就是当前语句。

  1. export interface StackEntry {
  2. id: number;
  3. line: number;
  4. name: string;
  5. file: string;
  6. }

这是fillStackTrace()方法的主要代码,它在来自调试服务器的回复上填充堆栈跟踪。

  1. let ln = Number(lines[i]);
  2. let file = lines[i + 1].trim();
  3. let line = lines[i + 2].trim();
  4. const entry = <StackEntry> {
  5. id: ++id, line : ln, name : line, file: file
  6. };
  7. this._stackTrace.push(entry);

CscsDebugger定期要求CscsRuntime提供堆栈跟踪。

  1. protected stackTraceRequest(): void {
  2. // ...
  3. const stk = this._runtime.stack(startFrame, endFrame);
  4. response.body = {
  5. stackFrames: stk.frames.map(f =>
  6. new StackFrame(f.index, f.name,
  7. this.createSource(f.file),
  8. this.convertDebuggerLineToClient(f.line))),
  9. totalFrames: stk.count
  10. };
  11. this.sendResponse(response);
  12. }

处理异常情况

如果客户端代码抛出了一个异常,调试服务器会用特殊的 exc 响应来通知你。CscsRuntime收到后,通过发送以下事件通知CscsDebug会话。

  1. this.sendEvent('stopOnException');

然后CscsDebug会话将此事件传播给框架。

  1. this._runtime.on('stopOnException', () => {
  2. this.sendEvent(new StoppedEvent('exception', CscsDebugSession.THREAD_ID));
  3. });

这样就可以在VS Code左侧窗格中显示异常信息和堆栈跟踪(见图4)。

在发生异常时,完整的堆栈跟踪会和异常一起从调试服务器接收。你只需要调用fillStackTrace()方法,其余的由框架来完成(详见上节)。

调试服务器的实现

你可以使用你为REPL评估器扩展实现的相同调试服务器。你只需要扩展它以处理更多的情况。需要扩展的函数是 ProcessClientCommand()。在 REPL 评估器的情况下,它只处理 repl 命令。清单 12 显示了扩展的 ProcessClientCommand() 的实现。

清单12:ProcessClientCommand() C#方法的一个片段

  1. void ProcessClientCommand(string data)
  2. {
  3. string [] parts = data.Split(new char [] { '|' });
  4. string cmd = parts[0].ToLower();
  5. string result = "N/A";
  6. SteppingIn = SteppingOut = false;
  7. SendBackResult = true;
  8. if (cmd == "file") {
  9. m_filename = data.Substring(cmd.Length + 1);
  10. string rawScript = Utils.GetFileContents(m_filename);
  11. m_script = Utils.ConvertToScript(rawScript, out m_char2Line);
  12. m_debugging = new ParsingScript (m_script, 0, m_char2Line);
  13. } else if (cmd == "setbp") {
  14. Breakpoints.Instance.AddBreakpoints(this, data);
  15. return;
  16. } else if (cmd == "repl") {
  17. result = ProcessRepl(data.Substring(cmd.Length + 1));
  18. SendBack(result);
  19. return;
  20. } else if (cmd == "continue") {
  21. Continue = true;
  22. cmd = "next";
  23. } else if (cmd == "stepin") {
  24. SteppingIn = true;
  25. cmd = "next";
  26. }
  27. if (cmd == "next") {
  28. Variable res = ProcessNext();
  29. int origLineNumber = GetCurrentLineNumber();
  30. string filename = GetCurrentFilename ();
  31. result = CreateResult(filename, origLineNumber, Output, processedStr);
  32. }
  33. result = cmd + "\n" + result;
  34. SendBack(result);
  35. }

请注意,没有具体的命令来获取本地/全局变量和执行堆栈的请求。原因是,服务器在每次请求时都会发回变量和堆栈信息。清单13显示了CreateResult()方法的实现,该方法将所有这些信息添加到要发送给客户端的响应中。

清单13:CreateResult() C#方法

  1. string CreateResult(string filename, int lineNumber,
  2. string output, string processed = "")
  3. {
  4. int outputCount = output.Split('\n').Length;
  5. string result = filename + "\n";
  6. result += lineNumber + "\n";
  7. result += outputCount + "\n";
  8. result += output + "\n";
  9. string vars = GetVariables();
  10. int varsCount = vars.Split('\n').Length;
  11. result += varsCount + "\n";
  12. result += vars + "\n";
  13. string stack = GetStack();
  14. result += stack + "\n";
  15. return result;

调试服务器的实现是针对CSCS语言的。看看GitHub上的实现和附带的源代码下载,并根据你的语言来调整它。

结束语

可以说,Visual Studio Code是作为Eclipse集成开发环境的替代品出现的,Erich Gamma早先参与了Eclipse的开发,这并不是偶然的。事实上,越来越多的开发者正在从Eclipse转向VS Code。我发现VS Code比Eclipse更容易扩展,而且我还感觉VS Code是一个比Eclipse更轻巧的工具。

VS Code可以扩展其功能并支持新的语言。在这篇文章中,你看到了如何以两种方式扩展VS Code的功能:如何添加扩展和如何添加新的语言支持,包括语言支持的主要任务之一—调试。

我希望你喜欢使用REPL扩展动态创建移动应用程序的可能性。

你在本文中看到的所有项目的所有代码都是开源的。请看侧边栏中附带的源代码下载和GitHub链接,以获得最新的发展。

我想特别感谢微软的Stefan Landvogt为我提供了无价的技巧和建议。

参考资料

CSCS和VS代码扩展 源代码

文章原地址

https://www.codemag.com/article/1809051/Writing-Your-Own-Debugger-and-Language-Extensions-with-Visual-Studio-Code

镜像地址

https://web.archive.org/web/20211029113804/https://www.codemag.com/article/1809051/Writing-Your-Own-Debugger-and-Language-Extensions-with-Visual-Studio-Code