diff --git a/CHANGELOG.md b/CHANGELOG.md index 896b9d5..1a04acc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.0] - 2021-01-26 ๐Ÿงจ๐Ÿ‚ + +### Added + +- Support for debugger adapter. Code can be evaluated and symbols can be +shown in the editor. + +- CodeAction to show Documentation for system symbols (Thanks @wuyudi for +the help with testing) + ## [0.2.2] - 2020-08-01 ๐Ÿฑโ€๐Ÿ ### Added @@ -94,7 +104,7 @@ error will not popup in Output window in VSCode (reported by - SVG image for document information -## [0.1.1] - 2019-02-05 ๐Ÿงง +## [0.1.1] - 2019-02-05 ๐Ÿงง๐Ÿ– ### Added diff --git a/LICENSE b/LICENSE index 51c71cd..fdd4ab3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 lsp-wl +Copyright (c) 2018 lsp-wl Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 0661f39..b3d1aef 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ [![Develop with: Wolfram Language](https://img.shields.io/badge/Develop%20with-Wolfram%20Language-%23d81013.svg)](http://www.wolfram.com/language/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![CI](https://github.com/kenkangxgwe/lsp-wl/workflows/CI/badge.svg?branch=master)](https://github.com/kenkangxgwe/lsp-wl/actions?query=workflow%3ACI+branch%3Amaster) ![WolframLanguageServerLogo](images/wolfram-language-server-logo-clipped.png) -> by [kenkangxgwe](https://github.com/kenkangxgwe) and [hxianglong](https://github.com/huxianglong) **Table of Contents** @@ -12,7 +12,19 @@ - [Wolfram Language Server](#wolfram-language-server) - [Installation](#installation) - [Run the Server](#run-the-server) - - [Features](#features) + - [Language Server Features](#language-server-features) + - [DocumentSymbol](#documentsymbol) + - [Hover](#hover) + - [Completion](#completion) + - [Diagnostics](#diagnostics) + - [Definition / References / Document Highlight](#definition--references--document-highlight) + - [Code Action](#code-action) + - [Document Color / Color Presentation](#document-color--color-presentation) + - [Notes](#notes) + - [Debug Adapter Features](#debug-adapter-features) + - [Evaluate](#evaluate) + - [Variables](#variables) + - [Notes](#notes-1) - [Contribute](#contribute) - [Design Principles](#design-principles) - [Todo list](#todo-list) @@ -24,17 +36,18 @@ **Wolfram Language Server (WLServer)** is an implementation of the Microsoft's [Language Server Protocol (LSP)](https://microsoft.github.io/language-server-protocol) for [Wolfram -Language](http://www.wolfram.com/language). This server is -implemented in Wolfram Language itself. +Language](http://www.wolfram.com/language). This server is implemented in +Wolfram Language itself. -Our current goal is to provide the experience as good as the Mathematica FrontEnd -with addition power from the editor. +Our current goal is to provide the experience as good as the Mathematica +FrontEnd with addition power from the editor. -We have provided the client-side code for VS Code [here](https://github.com/kenkangxgwe/vscode-lsp-wl), which is based on some slight -modifications of [Microsoft's LSP +We have provided the client-side code for VS Code +[here](https://github.com/kenkangxgwe/vscode-lsp-wl), which is based on some +slight modifications of [Microsoft's LSP example](https://github.com/Microsoft/vscode-extension-samples/tree/master/lsp-sample). -If you are using other editors supporting LSP, some adaptation to the -client would certainly work too. +If you are using other editors supporting LSP, some adaptation to the client +would certainly work too. ## Installation @@ -47,8 +60,8 @@ client would certainly work too. git clone https://github.com/kenkangxgwe/lsp-wl.git ``` -2. Install the dependent paclets with the correct versions (currently 1.0) -from the Wolfram kernel / Mathematica. +2. Install the dependent paclets with the correct versions (currently 1.0 or +later) from the Wolfram kernel / Mathematica. (_This will cost some time for the first time_) : ``` mathematica PacletInstall["CodeParser"] @@ -56,11 +69,14 @@ from the Wolfram kernel / Mathematica. ``` 3. Install the client. Currently, we provide the VS Code extension on [Visual -Studio Marketplace: Wolfram Language Server](https://marketplace.visualstudio.com/items?itemName=lsp-wl.lsp-wl-client) +Studio Marketplace: Wolfram Language +Server](https://marketplace.visualstudio.com/items?itemName=lsp-wl.lsp-wl-client) +For other editors, please refer to the +[wiki](https://github.com/kenkangxgwe/lsp-wl/wiki). 4. You may also want to install -[GitLink](https://github.com/WolframResearch/GitLink) packet in order to -check for updates. +[GitLink](https://github.com/WolframResearch/GitLink) packet in order to check +for updates. ## Run the Server @@ -78,11 +94,11 @@ The posible arguments for the server are - `--help, -h` to print help information. - `--socket=port` to assign the port to which the server will connect. (Default: `6536`) -- `--tcp-server=port` to assign the port at which the server will start. (Default: -`6536`) +- `--tcp-server=port` to assign the port at which the server will start. +(Default: `6536`) - `--pipe=pipename` to specify the pipe name for the server to connect to. -- `--log=level, -l level` to specify the logging level of the server. - (Levels: `error`, `warn`, `info`, `debug`. Default: `info`) +- `--log=level, -l level` to specify the logging level of the server. (Levels: + `error`, `warn`, `info`, `debug`. Default: `info`) - `--test, -t` to run the unit test for the server. If you want to run the server from Mathematica you can use the following code. @@ -95,89 +111,148 @@ Block[{$ScriptCommandLine = Prepend[args, initfile], Quit = Function[{}, Throw[N ]; ``` -This is a good way to see the results from the unit tests. +To use the debugger adapter (current in a very early stage), you need to include +`debuggerPort` in the `initializationOptions`. And attach the frontend to that +port. For VS Code, it is automatically done by the extension. -## Features +## Language Server Features -- **DocumentSymbol:** You may typeset your package in the same way that - Mathematica FrontEnd handles it: a cell begins with two lines of comments, - where the first line specifies the style of the cell and the second line names it. - So you may get the outline structure of the file. - - ``` mathematica - (* ::Title:: *) - (*Title of the file*) +### DocumentSymbol - (* ::Section:: *) - (*Section 1*) - ``` - - ![documentSymbol](images/documentSymbol.png) +You may typeset your package in the same way that Mathematica FrontEnd handles +it: a cell begins with two lines of comments, where the first line specifies the +style of the cell and the second line names it. So you may get the outline +structure of the file. + +``` mathematica +(* ::Title:: *) +(*Title of the file*) + +(* ::Section:: *) +(*Section 1*) +``` + +![documentSymbol](images/documentSymbol.png) + +### Hover + +Provide documentations for functions and variables from the ``System` `` +context, such as `String` and `$Path`, the `MessageName` and the special +numerical literals with `^^` or `*^`. + +![hover](images/hover.png) + +### Completion + +The completion is shown by the client automatically. Functions and system +variables from the ``System` `` context that matches the input would be +displayed. To enter an unicode character, you may use the leader key +\\ followed by the alias just like esc in Wolfram +FrondEnd. E.g., `a` in the FrontEnd is input as `\a` in the editor and the +server will show you the available completions. + +![completion-unicode](images/completion_alias.png) + +**Completion Resolve:** Further information (such as documentation) would be +provided for the items in the list. + +![completion](images/completion.png) -- **Hover:** Provide documentations for functions and variables from the - ``System` `` context, such as `String` and `$Path`, the `MessageName` and - the special numerical literals with `^^` or `*^`. +### Diagnostics - ![hover](images/hover.png) +Syntax error would be underlined. This feature is powered by +[CodeParser](https://github.com/WolframResearch/codeparser) and +[CodeInspector](https://github.com/WolframResearch/codeinspector) paclets, thank +you [@bostick](https://github.com/bostick). -- **Completion:** The completion is shown by the client automatically. - Functions and system variables from the ``System` `` context that matches the - input would be displayed. To enter an unicode character, you may use the - leader key \\ followed by the alias just like esc in - Wolfram FrondEnd. E.g., `a` in the FrontEnd is input as `\a` in the - editor and the server will show you the available completions. +![diagnostics](images/diagnostics.png) - ![completion-unicode](images/completion_alias.png) +### Definition / References / Document Highlight -- **Completion Resolve:** Further information (such as documentation) would be - provided for the items in the list. +It is now able to look up the definition and references of a local variable in a +scope such as `Module` or pattern rules. - ![completion](images/completion.png) +![references](images/references.png) -- **Diagnostics:** Syntax error would be underlined. This feature is powered - by [CodeParser](https://github.com/WolframResearch/codeparser) and - [CodeInspector](https://github.com/WolframResearch/codeinspector) paclets, - thank you [@bostick](https://github.com/bostick). +### Code Action - ![diagnostics](images/diagnostics.png) +Code action is now able to, -- **Definition / References / DocumentHighlight:** It is now able to look up the - definition and references of a local variable in a scope such as `Module` or - pattern rules. +- Open the documentation of system symbols in Mathematica (Not available for + Wolfram Engine). + ![documentation](images/codeActionSymbolDocumentation.png) - ![references](images/references.png) +- Evaluate the selected code if debugger is running. See [Evaluate](#evaluate). -- **Document Color / Color Presentation:** Both Named Colors and - Color Models with constant parameters are able to show and modify. - (_Experimental, may have performance issues._) +### Document Color / Color Presentation - ![documentColor](images/documentColor.png) +Both Named Colors and +Color Models with constant parameters are able to show and modify. +(_Experimental, may have performance issues._) -This is an early release, so more features are on the way. Notice that, -syntax highlight will not be provided as long as it is excluded in the LSP, -but I believe there are plenty of good Mathematica highlighter available for -your editor. +![documentColor](images/documentColor.png) -Here is a full list of [LSP features](https://microsoft.github.io/language-server-protocol/specification). +### Notes + +The project is under development, so more features are on the way. Notice that, +**syntax highlight** will not be provided as long as it is excluded from the +LSP, but I believe there are already plenty of good Mathematica highlighters +available for your editor. + +Here is a full list of [LSP +features](https://microsoft.github.io/language-server-protocol/specification). + +## Debug Adapter Features + +### Evaluate + +Code evaluation can be run from the code action of the selection or code lens +below each section title. The results are usually shown in the debug console on +the editor side. + +![evaluate-code-action](images/evaluate_code_action.png) + +Expressions can also be directly input from the debug console. + +![evaluate-debug-console](images/evaluate_debug_console.png) + +### Variables + +After evaluation, the symbol values can be retrieved from the editor. This +includes the own values of variables and the down/up/sub values of functions +defined. + +![variables](images/variables.png) + +The behavior of the variables mimics the workspace in MATLAB, so all the symbols +defined in the debug console as well as evaluated from the file will be +recorded. This also includes contexts other than ``Global` ``. The editor can +also watch on a specific expression after each evaluation if applicable. + +### Notes + +Here is a full list of [DAP +features](https://microsoft.github.io/debug-adapter-protocol/specification). ## Contribute ### Design Principles 1. The files are located according to its context name. The `init.wls` is the - entry script that parses the commandline arguments, loads packages - and starts the server. + entry script that parses the commandline arguments, loads packages and starts + the server. This is intended to be different from a paclet, since it is not + intended to be normally used inside Mathematica / Wolfram Kernel. -2. We implemented an stateless server in ``WolframLanguageServer`Server` `` that - will parse and handle the messages. +2. We implemented a *stateless* (by passing the state around :)) server in + ``WolframLanguageServer`Server` `` that will parse and handle messages. -3. ``DataType` `` is a simple type system now extracted as a independent - package in the [Matypetica](https://github.com/kenkangxgwe/Matypetica) - library that supports pattern test on every field of a class. The operations - on the objects are designed to be immutable. +3. ``DataType` `` is a simple type system extracted as an independent package in + the [Matypetica](https://github.com/kenkangxgwe/Matypetica) library that + supports pattern test on every field of a data structure. The operations on + the data objects are designed to be immutable. -4. ``WolframLanguageServer`Test`* `` stores the unit tests for some of - the functions. +4. ``WolframLanguageServer`Test`* `` stores the unit tests for some of the + functions which are integrated into GitHub Action. ### Todo list @@ -188,8 +263,8 @@ It will be nice if you want to make a contribution to the following topic. but it fails to send responses back to the client. * It will be helpful to implement a stdio channel, ~so that the Mathematica - earlier than 11.2 will also be supported,~ but it is really hard to expose - the `stdin` channel. Hope this will be dicussed in future release of Wolfram + earlier than 11.2 will also be supported,~ but it is really hard to expose the + `stdin` channel. Hope this will be dicussed in future release of Wolfram kernel. * More editor clients are needed. You can feel free to open a repository and @@ -197,10 +272,12 @@ It will be nice if you want to make a contribution to the following topic. released. * Thanks to [CodeParser](https://github.com/WolframResearch/codeparser) and - [CodeInspector](https://github.com/WolframResearch/codeinspector) paclets, - we are able to parse the code and extract useful information. If you have - an idea about how to use these fantastic syntax tools to help the language - server add more features, please send us issues or pull requests. + [CodeInspector](https://github.com/WolframResearch/codeinspector) paclets, we + are able to parse the code and extract useful information. Please consider to + contribute to them as well. + + If you have ideas about how to use these fantastic language tools to help the + language server with more features, please send us issues or pull requests. If you want to help us with this project, feel free to fork and create a pull request. Do not forget to add unit tests if possible. @@ -208,9 +285,9 @@ request. Do not forget to add unit tests if possible. ## Donations :dollar: If you really like this project, please donate to us! **$5 (or equivalently -๏ฟฅ35)**. A cup of coffee :coffee: would certainly -brighten our day! Your donation would be the motivation for us to move forward, -thanks in advance :smile:. +๏ฟฅ35)**. A cup of coffee :coffee: would certainly brighten our day! Your +donation would be the motivation for us to move forward, thanks in advance +:smile:. - Paypal: qwe95123@126.com - Alipay (With QRCode): 13916018006 diff --git a/external/Matypetica/src/DataType.wl b/external/Matypetica/src/DataType.wl index d864ded..f4b1763 100644 --- a/external/Matypetica/src/DataType.wl +++ b/external/Matypetica/src/DataType.wl @@ -25,8 +25,7 @@ ReplaceKeyBy[replaceRule_Rule] is an operator that can be applied to an object." DeleteKey::usage = "DeleteKey[object, key] deletes the key-value pair from object[key]. DeleteKey[object, {key1, key2}] deletes the key-value pair at object[key1][key2]. DeleteKey[keys] is an operator that can be applied to an object." -TypeCheckOn::usage = "TypeCheckOn[] turns on type checking." -TypeCheckOff::usage = "TypeCheckOff[] turns off type checking." +TypeCheck::usage = "TypeCheck[toggle_?BooleanQ] turns on/off the type checking." Begin["`Private`"] @@ -42,10 +41,22 @@ DeclareType[typename_Symbol, typekey:<|(_String -> _)...|>] := Module[ }, (* Getter *) - typename[typedict_Association][key_String] := TypeCheck[typedict[key], typekey[key]]; + typename[typedict_Association][key_String] := ( + If[$typeCheckQ, + Which[ + MissingQ[typedict[key]], + Null, + !KeyMemberQ[typekey, key], + Message[TypeCheck::miskey, typename, key], + !MatchQ[typedict[key], typekey[key]], + Message[TypeCheck::mispat, typename, key, typedict[key]] + ] + ]; + typedict[key] + ); typename[typedict_Association][key_String, default_] := With[ {value = typename[typedict][key]}, - If[MissingQ[value], TypeCheck[default, typekey[key]], value] + If[MissingQ[value], typename[<|key -> default|>][key], value] ]; (* Deserializer*) @@ -187,14 +198,11 @@ ConstructType[parameters_Association, pattern:Association[(Verbatim[Repeated]|Ve (*TypeCheck*) -TypeCheckOff[] := (ClearAll[TypeCheck]; TypeCheck[v_, ___] := v) -TypeCheckOn[] := ( - ClearAll[TypeCheck]; - TypeCheck[v_?MissingQ, _] := v; - TypeCheck[_, p_?MissingQ] := p; - TypeCheck[val_, pat_] := If[MatchQ[val, pat], val, Missing["PatternMismatch", {val, pat}]]; -) -TypeCheckOn[] +TypeCheck::miskey = "`1` doesn't contains key `2`." +TypeCheck::mispat = "`3` doesn't match the pattern of \"`2`\" in `1`." + +TypeCheck[toggle_?BooleanQ] := ($typeCheckQ = toggle) +TypeCheck[True] (* ::Section:: *) diff --git a/external/Matypetica/test/DataTypeTest.wl b/external/Matypetica/test/DataTypeTest.wl index 2eb206f..d06af06 100644 --- a/external/Matypetica/test/DataTypeTest.wl +++ b/external/Matypetica/test/DataTypeTest.wl @@ -58,14 +58,15 @@ VerificationTest[ VerificationTest[ stu1 = Student[<|"id" -> 1, "name" -> "John Doe", "sex" -> "Man"|>]; stu1["sex"], - Missing["PatternMismatch", {"Man", "Male"|"Female"}], + "Man", + {TypeCheck::mispat}, TestID -> "Getter Type Check" ], VerificationTest[ - TypeCheckOff[]; + TypeCheck[False]; stu1 = Student[<|"id" -> 1, "name" -> "John Doe", "sex" -> "Man"|>]; - With[{res = stu1["sex"]}, TypeCheckOn[]; res], + With[{res = stu1["sex"]}, TypeCheck[True]; res], "Man", TestID -> "Getter Type Check Off" ], diff --git a/images/codeActionSymbolDocumentation.png b/images/codeActionSymbolDocumentation.png new file mode 100644 index 0000000..c17704b Binary files /dev/null and b/images/codeActionSymbolDocumentation.png differ diff --git a/images/evaluate_code_action.png b/images/evaluate_code_action.png new file mode 100644 index 0000000..18865f4 Binary files /dev/null and b/images/evaluate_code_action.png differ diff --git a/images/evaluate_debug_console.png b/images/evaluate_debug_console.png new file mode 100644 index 0000000..668cd1b Binary files /dev/null and b/images/evaluate_debug_console.png differ diff --git a/images/variables.png b/images/variables.png new file mode 100644 index 0000000..9df0bd3 Binary files /dev/null and b/images/variables.png differ diff --git a/init.wls b/init.wls index 1e383f1..fc0c0f2 100644 --- a/init.wls +++ b/init.wls @@ -1,21 +1,23 @@ #!/usr/bin/env wolframscript (* ::Package:: *) -(* Wolfram Language Server *) -(* Author: kenkangxgwe , - huxianglong -*) +(* Copyright 2018 lsp-wl Authors *) +(* SPDX-License-Identifier: MIT *) -(* init.wl +(* init.wls This is a script file to initialize the Wolfram Language Server. Please see the help info below. *) + (* For windows, this should be set for Print to correctly format outputs. *) -SetOptions[$Output, FormatType -> OutputForm] +If[$OperatingSystem == "Windows" && $FrontEnd === Null, + SetOptions[$Output, FormatType -> OutputForm] +] + (* ::Subsection:: *) (*RootDirectory*) @@ -95,6 +97,7 @@ Options: --test, -t Run all tests --log=loglevel, -l loglevel Specifiy logging level as debug, info, warn or error (default: info) + --no-start Do not run the language server. (Useful when running in notebook) --socket=port Connect to a socket server on port (default: 6536) --tcp-server=port Start a socket server on port (default: 6536) --pipe=pipeName Connect via a named pipe (Windows only) @@ -115,10 +118,10 @@ Options: (*Version*) -WolframLanguageServer`Version = "0.2.2" +WolframLanguageServer`$Version = "0.2.2" If[MemberQ[WolframLanguageServer`CommandLine, "-v" | "--version"], Print[" -Wolfram Language Server " <> WolframLanguageServer`Version <> " running on +Wolfram Language Server " <> WolframLanguageServer`$Version <> " running on Wolfram Language " <> $Version <> "\n"]; Quit[]; ]; @@ -211,12 +214,14 @@ Module[ *) << WolframLanguageServer`Server`; WolframLanguageServer`CheckReturnTypeQ = True; - LogDebug @ WolframLanguageServer`Server`WLServerStart[ - "Stream" -> stream, - "ClientPid" -> clientPid, - "Port" -> port, - "Pipe" -> pipe, - "WorkingDir" -> WolframLanguageServer`RootDirectory + If[!MemberQ[WolframLanguageServer`CommandLine, "--no-start"], + LogDebug @ WolframLanguageServer`Server`WLServerStart[ + "Stream" -> stream, + "ClientPid" -> clientPid, + "Port" -> port, + "Pipe" -> pipe, + "WorkingDir" -> WolframLanguageServer`RootDirectory + ] ]; Quit[0] ]; diff --git a/src/PatternTemplate.wl b/src/PatternTemplate.wl index 915cbd3..968cd4b 100644 --- a/src/PatternTemplate.wl +++ b/src/PatternTemplate.wl @@ -1,7 +1,10 @@ (* ::Package:: *) +(* Copyright 2020 lsp-wl Authors *) +(* SPDX-License-Identifier: MIT *) + + (* Pattern Template *) -(* Author: kenkangxgwe *) BeginPackage["PatternTemplate`"] @@ -57,7 +60,7 @@ PatternTemplateObject[templatedPattern_, o:OptionsPattern[]][newPatterns___] := // PatternTemplateObject[templatedPattern] ) -PatternTemplateObject[templatedPattern_, o:OptionsPattern[]][newPatterns_Association] := Block[ +PatternTemplateObject[templatedPattern_, o:OptionsPattern[]][newPatterns_Association] := With[ { newPatternTuples = Replace[ newPatterns, diff --git a/src/WolframLanguageServer/Adaptor.wl b/src/WolframLanguageServer/Adaptor.wl new file mode 100644 index 0000000..d5db6e6 --- /dev/null +++ b/src/WolframLanguageServer/Adaptor.wl @@ -0,0 +1,120 @@ +(* ::Package:: *) + +(* Copyright 2020 lsp-wl Authors *) +(* SPDX-License-Identifier: MIT *) + + +(* Wolfram Language Server Debugger Adaptor *) + + +BeginPackage["WolframLanguageServer`Adaptor`"] +ClearAll[Evaluate[Context[] <> "*"]] + + +(* Output Symbols *) +CreateDebuggerKernel::usage = "Create a subkernel as a debugger, and returns the KernelObject." +CloseDebuggerKernel::usage = "CloseDebuggerKernel[_KernelObject] closed the specified subkernel." +GetKernelId::usage = "GetKernelId[kernel_KernelObject] returns the $KernelID of the specified kernel." +GetProcessId::usage = "GetProcessId[kernel_KernelObject] returns the $ProcessID of the specified kernel." +GetThreads::usage = "GetThreads[kernel_KernelObject] returns the available thread of the specified kernel. \ +(Currntly only one thread is available.)" +GetStackFrames::usage = "GetStackFrames[stackTraceArguments_Association, kernel_KernelObject] \ +returns the stackframes of the specified thread." +GetScopes::usage = "GetScopes[scopesArguments_Association, kernel_KernelObject] returns the scopes of the specified stackframe." +GetVariables::usage = "GetVariables[variablesArguments_Association, kernel_KernelObject] returns the variables according to the arguments." +DebuggerEvaluate::usage = "DebuggerEvaluate[evaluateArguments_Association, kernel_KernelObject] \ +evaluate the given expr in the debugger and return the string form." + + +(* Private Context *) +Begin["`Private`"] +ClearAll[Evaluate[Context[] <> "*"]] + + +Needs["DataType`"] +Needs["WolframLanguageServer`Logger`"] +Needs["WolframLanguageServer`Specification`"] +Needs["WolframLanguageServer`Debugger`"] + + +$DistributedContexts = "WolframLanguageServer`Debugger`" + + +CreateDebuggerKernel[] := With[ + { + subKernel = LaunchKernels[1] // First, + path = $Path + }, + ParallelEvaluate[ + $Path = path; + Needs["WolframLanguageServer`Debugger`"]; + DebuggerInit[], + subKernel + ]; + subKernel +] + + +GetKernelId[kernel_KernelObject] := ParallelEvaluate[$KernelID, kernel] + + +GetProcessId[kernel_KernelObject] := ParallelEvaluate[$ProcessID, kernel] + + +GetThreads[kernel_KernelObject] := { + DapThread[<| + "id" -> 1, + "name" -> "default" + |>] +} + + +GetStackFrames[stackTraceArguments_Association, kernel_KernelObject] := { + StackFrame[<| + "id" -> 0, + "name" -> "default", + "line" -> 0, + "column" -> 0 + |>] +} + + +GetScopes[scopesArguments_Association, kernel_KernelObject] := ( + ParallelEvaluate[GetContextsReferences[], kernel] +) + + +GetVariables[variablesArguments_Association, kernel_KernelObject] := ( + ParallelEvaluate[GetVariablesReference[variablesArguments], kernel] +) + + +DebuggerEvaluate[evaluateArguments_Association, kernel_KernelObject] := ( + If[evaluateArguments["context"] === "variables", + evaluateArguments["expression"], + evaluateArguments["expression"] + // StringTrim + // ParallelEvaluate[ + (* Keeps Stack[] clean *) + ToExpression[#, InputForm, Hold] + // ReleaseHold, + kernel, + (* This will save all the results (per line) into a sequence. *) + Hold + ]& + // Replace[$Failed -> Hold[]] + // DeleteCases[Null] + (* Do not let the results evaluate anymore in the adaptor-side *) + // Map[Unevaluated] + // Apply[List] + // Map[ToString] + // StringRiffle[#, "\n"]& + // (ParallelEvaluate[GetContextsReferences[], kernel]; #)& + ] +) + + +End[] + + +EndPackage[] \ No newline at end of file diff --git a/src/WolframLanguageServer/Archives.wl b/src/WolframLanguageServer/Archives.wl index bfa6595..8ded83a 100644 --- a/src/WolframLanguageServer/Archives.wl +++ b/src/WolframLanguageServer/Archives.wl @@ -1,10 +1,10 @@ (* ::Package:: *) -(*Archives*) + +(* Copyright 2019 lsp-wl Authors *) +(* SPDX-License-Identifier: MIT *) + (* Wolfram Language Server Archived Functions *) -(* Author: kenkangxgwe , - huxianglong -*) BeginPackage["WolframLanguageServer`Archives`"] diff --git a/src/WolframLanguageServer/AstPatterns.wl b/src/WolframLanguageServer/AstPatterns.wl index f92d408..5074df9 100644 --- a/src/WolframLanguageServer/AstPatterns.wl +++ b/src/WolframLanguageServer/AstPatterns.wl @@ -1,9 +1,11 @@ (* ::Package:: *) +(* Copyright 2019 lsp-wl Authors *) +(* SPDX-License-Identifier: MIT *) + + (* Wolfram Language Server Ast Patterns *) -(* Author: kenkangxgwe , - huxianglong -*) + BeginPackage["WolframLanguageServer`AstPatterns`"] ClearAll[Evaluate[Context[] <> "*"]] diff --git a/src/WolframLanguageServer/ColorTable.wl b/src/WolframLanguageServer/ColorTable.wl index ecce2a1..53efb2b 100644 --- a/src/WolframLanguageServer/ColorTable.wl +++ b/src/WolframLanguageServer/ColorTable.wl @@ -1,9 +1,11 @@ (* ::Package:: *) +(* Copyright 2019 lsp-wl Authors *) +(* SPDX-License-Identifier: MIT *) + + (* Wolfram Language Server Color Table *) -(* Author: kenkangxgwe , - huxianglong -*) + BeginPackage["WolframLanguageServer`ColorTable`"] ClearAll[Evaluate[Context[] <> "*"]] diff --git a/src/WolframLanguageServer/Debugger.wl b/src/WolframLanguageServer/Debugger.wl new file mode 100644 index 0000000..b9af83a --- /dev/null +++ b/src/WolframLanguageServer/Debugger.wl @@ -0,0 +1,339 @@ +(* ::Package:: *) + +(* Copyright 2020 lsp-wl Authors *) +(* SPDX-License-Identifier: MIT *) + + +(* Wolfram Language Server Debugger *) + + +BeginPackage["WolframLanguageServer`Debugger`"] +ClearAll[Evaluate[Context[] <> "*"]] + + +(* Output Symbols *) +DebuggerInit::usage = "DebuggerInit[] initializes the debugger with an empty symbol table to record every symbol defined." +GetContextsReferences::usage = "GetContextsReferences[] refreshes contexts imported in the debugger." +GetVariablesReference::usage = "GetVariablesReference[variablesArguments_Association] returns a set of variables regarding the specified arguments." + +(* Private Context *) +Begin["`Private`"] +ClearAll[Evaluate[Context[] <> "*"]] + + +Needs["DataType`"] +Needs["WolframLanguageServer`Logger`"] +Needs["WolframLanguageServer`Specification`"] + + +$ExpensiveContexts = {"System`"} + + +DebuggerInit[] := ( + $SymbolTable = <|"Global`" -> {}|>; + $ReferenceTable = <||>; + $NewSymbol = (($SymbolTable = + Merge[{$SymbolTable, <|#2 -> #1|>}, Flatten] + )&) +) + + +appendReference[type_, name_String, pos_:Nothing] := With[ + { + newKey = (Length[$ReferenceTable] + 1) + }, + + AssociateTo[$ReferenceTable, newKey -> ( + {type, name, pos} + )]; + newKey +] + + +GetContextsReferences[] := ( + AssociateTo[$ReferenceTable, 1 -> {}]; + Table[ + Scope[<| + "name" -> context, + "variablesReference" -> appendReference["Context", context], + "expensive" -> If[MemberQ[$ExpensiveContexts, context], True, False], + If[MemberQ[$ExpensiveContexts, context], + "namedVariables" -> Length[Names[context <> "*"]], + Nothing + ] + |>], + {context, Keys[$SymbolTable]} + ] +) + + +(* Shall return the pre-side-effect results *) +GetVariablesReference[variablesArguments_Association] := ( + $ReferenceTable + // Key[variablesArguments["variablesReference"]] + // Replace[{ + _?MissingQ -> {}, + {"Context", context_String} :> ( + Names[context <> "*"] + // Map[analyzeSymbol] + // DeleteMissing + ), + {"Symbol", symbolName_String} :> ( + {analyzeSymbol[symbolName]} + // DeleteMissing + ), + {"AssocList", symbolName_String} :> ( + Table[ + symbolName + // ToExpression[#, InputForm, Unevaluated]& + // valuesFunc[#]& + // Replace[{ + valueList:Except[{}] :> ( + DapVariable[<| + "name" -> (valuesFunc // ToString), + "value" -> ( + If[valuesFunc === Attributes, + valueList, + (valueList // Keys) + ] // ToNonContextString[#]& + ), + "type" -> If[valuesFunc === Attributes, "List", "Rule List"], + "variablesReference" -> ( + appendReference["AssocValues", symbolName, valuesFunc] + ), + If[valuesFunc === Attributes, + "indexedVariables", + "namedVariables" + ] -> Length[valueList] + |>] + ), + {} -> Nothing + }], + {valuesFunc, {DownValues, SubValues, UpValues, Options, Attributes}} + ] + ), + { + "AssocValues", + symbolName_String, + valuesFunc:(OwnValues | DownValues | SubValues | UpValues | Options | Attributes) + } :> Block[ + { + values = symbolName + // ToExpression[#, InputForm, Unevaluated]& + // valuesFunc[#]& + }, + Table[ + createVariableWithTag[ + If[valuesFunc === Attributes, + index // ToString, + Part[values, index, 1] // ToNonContextString[#]& + ], + symbolName, + values // Extract[#, If[valuesFunc === Attributes, + {index}, + {index, 2} + ], Unevaluated]&, + {{valuesFunc, index}, {}} + ], + {index, Length[values]} + ] + ], + { + listType: "List" | "Association", + symbolName_String, + { + { + valuesFunc:(OwnValues | DownValues | SubValues | UpValues | Attributes | Options), + valueIndex_Integer + }, + {pos___} + } + } :> Block[ + { + list = symbolName + // ToExpression[#, InputForm, Unevaluated]& + // valuesFunc[#]& + // Extract[#, {valueIndex, 2, pos}, Unevaluated]& + }, + Table[ + createVariableWithTag[ + If[listType == "List", + index // ToString, + index // First // ToString + ], + symbolName, + list // Extract[#, index, Unevaluated]&, + {{valuesFunc, valueIndex}, {pos, index}} + ], + { + index, + If[listType == "List", + list + // Length[#]&, + list + // Keys[#]& + // Map[Key] + ] + } + ] + ], + _ -> {} + }] +) + + +SetAttributes[ToNonContextString, HoldFirst] + +(* + This will turn "x_pattern" into "Pattern[x, patter]". + A complete but slow solution is to parse the string with CodeParser and + delete contexts by source. +*) +ToNonContextString[expr_] := ( + expr + // Hold + // ReplaceAll[ + symbol_Symbol :> With[ + { + symbolName = SymbolName[Unevaluated[symbol]] + }, + symbolName + /; ( + (* Removes the context other than System` (operators) and Global` (not necessary) *) + {"System`", "Global`"} + // MemberQ[Context[symbol]] + // Not + )] + ] + // Extract[#, {1}, Unevaluated]& + // ToString[#]& +) + + +analyzeSymbol[symbolName_String] := ( + symbolName + // ToExpression[#, InputForm, Unevaluated]& + // Replace[{ + _?(Attributes /* MemberQ[ReadProtected]) -> Missing["NoValues"], + symbol_ :> Block[ + { + ownValues + }, + createVariableWithTag[ + SymbolName[symbol], + symbolName, + ownValues + // Extract[#, {1, 2}, Unevaluated]&, + {{OwnValues, 1}, {}} + ] + /; ( + symbol + // OwnValues + // If[# =!= {}, + ownValues = symbol // OwnValues; + True, + False + ]& + ) + ], + symbol_ :> Block[ + { + length + }, + DapVariable[<| + "name" -> SymbolName[symbol], + "value" -> "", + "type" -> "Function", + "variablesReference" -> appendReference["AssocList", symbolName], + "namedVariables" -> length, + "expensive" -> False + |>] + /; ( + symbol + // {DownValues, SubValues, UpValues} + // Through + // DeleteCases[{}] + // Length + // (length = #)& + // (# > 0)& + ) + ], + _ -> Nothing + }] +) + + +createVariableWithTag[tag_String, symbolName_String, expr_, nextPos_] := ( + expr + // Unevaluated + // analyzeExpr + // Apply[{value, type, length} \[Function] ( + DapVariable[<| + "name" -> (tag // ToString), + "value" -> (value), + "type" -> ( + type + // Replace["Symbol"|"Expression" -> "Variable"] + ), + "variablesReference" -> If[length == 0, + 0, + If[type == "Symbol", + appendReference["Symbol", value], + appendReference[type, symbolName, nextPos] + ] + ], + type + // Replace[{ + "List" -> ( + "indexedVariables" -> length + ), + "Association" | "Symbol" -> ( + "namedVariables" -> length + ), + "Expression" -> Nothing + }], + "expensive" -> False + |>] + )] +) + + +SetAttributes[analyzeExpr, HoldAll] +analyzeExpr[expr_] := ( + expr + // Unevaluated + // Replace[{ + list_?ListQ :> { + "List", + list // Length + }, + association_?AssociationQ :> { + "Association", + association + // Length + }, + symbol:Unevaluated[_Symbol] /; ( + symbol + // Attributes + // MemberQ[ReadProtected] + ) -> {"Expression", 0}, + symbol:Unevaluated[_Symbol] :> { + "Symbol", + symbol + // {OwnValues, DownValues, SubValues, UpValues} + // Through + // If[SameQ[#, Range[{}, 4]], 0, 1]& + }, + _ -> {"Expression", 0} + }] + // Prepend[ + expr + // ToNonContextString + ] +) + + +End[] + + +EndPackage[] \ No newline at end of file diff --git a/src/WolframLanguageServer/Logger.wl b/src/WolframLanguageServer/Logger.wl index e3fe62e..d0839ee 100644 --- a/src/WolframLanguageServer/Logger.wl +++ b/src/WolframLanguageServer/Logger.wl @@ -1,9 +1,10 @@ (* ::Package:: *) +(* Copyright 2018 lsp-wl Authors *) +(* SPDX-License-Identifier: MIT *) + + (* Wolfram Language Server Logger *) -(* Author: kenkangxgwe , - huxianglong -*) BeginPackage["WolframLanguageServer`Logger`"]; @@ -31,8 +32,12 @@ LoggerStart[level_, streams:{_OutputStream..}] := Module[ (Function[{lvl}, Function[{msg}, Function[{stream}, WriteString[stream, - Snippet["[" <> StringPadRight[ToUpperCase[lvl], 5] <> " " - <> DateString["ISODateTime"] <> "] " <> ToString[msg], 20] <> "\n" + StringJoin[ + StringJoin[ + "[", StringPadRight[ToUpperCase[lvl], 5], " ", DateString["ISODateTime"], "] ", ToString[msg] + ] + // Snippet[#, 20]&, + "\n"] ]] /@ streams; msg ] (*Echo[#, lvl, ##2]&*) diff --git a/src/WolframLanguageServer/Server.wl b/src/WolframLanguageServer/Server.wl index 07e8220..98380c4 100644 --- a/src/WolframLanguageServer/Server.wl +++ b/src/WolframLanguageServer/Server.wl @@ -1,9 +1,10 @@ (* ::Package:: *) +(* Copyright 2018 lsp-wl Authors *) +(* SPDX-License-Identifier: MIT *) + + (* Wolfram Language Server *) -(* Author: kenkangxgwe , - huxianglong -*) BeginPackage["WolframLanguageServer`Server`"]; @@ -26,14 +27,17 @@ Needs["WolframLanguageServer`Logger`"] Needs["WolframLanguageServer`Specification`"] Needs["WolframLanguageServer`TextDocument`"] Needs["WolframLanguageServer`Token`"] +Needs["WolframLanguageServer`Adaptor`"] (* ::Section:: *) (*Utility*) -FoldWhile[f_, x_, list_List, test_] := FoldWhile[f, Prepend[list, x], test]; -FoldWhile[f_, list_List, test_] := First[NestWhile[Prepend[Drop[#, 2], f @@ Take[#, 2]]&, list, Length[#] > 1 && test[First[#]]&]]; +If[$VersionNumber < 12.2, + FoldWhile[f_, x_, list_List, test_] := FoldWhile[f, Prepend[list, x], test]; + FoldWhile[f_, list_List, test_] := First[NestWhile[Prepend[Drop[#, 2], f @@ Take[#, 2]]&, list, Length[#] > 1 && test[First[#]]&]] +] (* ::Section:: *) @@ -45,11 +49,21 @@ DeclareType[WorkState, <| "openedDocs" -> _Association, (* (_DocumentUri -> _DocumentText)... *) "client" -> (_SocketClient | _SocketObject | _NamedPipe | _StdioClient | "stdio" | Null), "clientCapabilities" -> _Association, + "debugSession" -> _DebugSession, "scheduledTasks" -> {___ServerTask}, "caches" -> _Association, + "pendingServerRequests" -> _Association, "config" -> _Association |>] +DeclareType[DebugSession, <| + "initialized" -> _?BooleanQ, + "server" -> _SocketObject | Null, + "client" -> _SocketObject | Null, + "subKernel" -> _, + "context" -> _String, + "thread" -> _DapThread +|>] DeclareType[RequestCache, <| "cachedTime" -> _DateObject, @@ -69,8 +83,14 @@ InitialState = WorkState[<| "initialized" -> False, "openedDocs" -> <||>, "client" -> Null, + "debugSession" -> DebugSession[<| + "initialized" -> False, + "server" -> Null, + "client" -> Null + |>], "scheduledTasks" -> {}, "caches" -> initialCaches, + "pendingServerRequests" -> <||>, "config" -> <| "configFileConfig" -> loadConfig[] |> @@ -89,19 +109,25 @@ ServerCapabilities = <| "definitionProvider" -> True, "referencesProvider" -> True, "documentSymbolProvider" -> True, + "codeActionProvider" -> True, "documentHighlightProvider" -> True, + "codeLensProvider" -> <| + "resolveProvider" -> True + |>, "colorProvider" -> True, - (* "executeCommandProvider" -> <| + "executeCommandProvider" -> <| "commands" -> { - "openRef" + "openRef", + "dap-wl.evaluate-file", + "dap-wl.evaluate-range" } - |>, *) + |>, Nothing |> ServerConfig = <| "updateCheckInterval" -> Quantity[7, "Days"], - (* cached results *) + (* cached results *) "cachedRequests" -> { "textDocument/signatureHelp", "textDocument/documentSymbol", @@ -388,13 +414,26 @@ TcpSocketHandler[{stop_, state_WorkState}]:= ( CloseClient[state["client"]]; {stop, state} ) -TcpSocketHandler[state_WorkState] := Module[ +TcpSocketHandler[state_WorkState] := With[ { - client = state["client"] + client = state["client"], + debugSession = state["debugSession"] }, - If[SocketReadyQ[client], + Which[ + SocketReadyQ[client], handleMessageList[ReadMessages[client], state], + (* new client connected *) + debugSession["server"] =!= Null && + debugSession["client"] === Null && + Length[debugSession["server"]["ConnectedClients"]] > 0, + { + "Continue", + ReplaceKey[state, {"debugSession", "client"} -> First[debugSession["server"]["ConnectedClients"]]] + }, + debugSession["client"] =!= Null && SocketReadyQ[debugSession["client"]], + handleDapMessageList[ReadMessages[debugSession["client"]], state], + True, doNextScheduledTask[state] ] // Replace[{ @@ -449,7 +488,7 @@ StreamPattern = ("stdio"|_SocketObject); SelectClient[connection_SocketObject] := ( - SelectFirst[connection["ConnectedClients"], SocketReadyQ] (* SocketWaitNext does not work in 11.3 *) + SelectFirst[connection["ConnectedClients"], SocketReadyQ] (* SocketWaitNext does not work in 11.3 *) // (If[MissingQ[#], Pause[1]; Return[SelectClient[connection]], @@ -635,7 +674,7 @@ RPCPatterns = <| constructRPCBytes[msg_Association] := ( Check[ - ExportByteArray[msg, "RawJSON"], + ExportByteArray[msg, "RawJSON", "Compact" -> True], (* if the result is not able to convert to JSON, returns an error respond @@ -648,7 +687,9 @@ constructRPCBytes[msg_Association] := ( "InternalError", "The request is not handled correctly." ]], - "RawJSON"] + "RawJSON", + "Compact" -> True + ] ] // { (* header *) Length @@ -692,10 +733,11 @@ CloseClient[client_NamedPipe] := With[ NotificationQ = KeyExistsQ["id"] /* Not (* NotificationQ[msg_Association] := MissingQ[msg["id"]] *) +ResponseQ = And[KeyExistsQ["method"] /* Not, KeyExistsQ["id"]] /* Through handleMessageList[msgs:{___Association}, state_WorkState] := ( FoldWhile[handleMessage[#2, Last[#1]]&, {"Continue", state}, msgs, MatchQ[{"Continue", _}]] -); +) handleMessage[msg_Association, state_WorkState] := With[ { @@ -706,19 +748,22 @@ handleMessage[msg_Association, state_WorkState] := With[ (* wrong message before initialization *) !state["initialized"] && !MemberQ[{"initialize", "initialized", "exit"}, method], If[!NotificationQ[msg], - sendResponse[state["client"], <| + sendMessage[state["client"], ResponseMessage[<| "id" -> msg["id"], "error" -> ServerError[ "ServerNotInitialized", "The server is not initialized." ] - |>] + |>]] (* otherwise, dropped the notification *) ]; {"Continue", state}, (* notification*) NotificationQ[msg], handleNotification[method, msg, state], + (* response *) + ResponseQ[msg], + handleResponse[state["pendingServerRequests"][msg["id"]], msg, state], (* resquest *) True, Which[ @@ -732,7 +777,47 @@ handleMessage[msg_Association, state_WorkState] := With[ handleRequest[method, msg, state] ] ] -]; +] + + +handleDapMessageList[msgs:{___Association}, state_WorkState] := ( + FoldWhile[handleDapMessage[#2, Last[#1]]&, {"Continue", state}, msgs, MatchQ[{"Continue", _}]] +) + +handleDapMessage[msg_Association, state_WorkState] := Module[ + { + newState = state + }, + + LogDebug["handleDapMessage: " <> ToString[msg]]; + + Which[ + (* wrong message before initialization *) + !state["debugSession"]["Initialized"] && + msg["type"] != "request" && + !MemberQ[{"initialize"}, msg["command"]], + If[msg["type"] == "request", + sendMessage[state["dubugSession"]["client"], DapResponse[<| + "type" -> "response", + "request_seq" -> msg["seq"], + "success" -> False, + "command" -> msg["command"], + "message" -> "ServerNotInitialized", + "body" -> <| + "error" -> "The server is not initialized." + |> + |>]] + (* otherwise, dropped the notification *) + ]; + {"Continue", state}, + (* event*) + msg["type"] == "event", + handleDapEvent[msg["event"], msg, newState], + (* resquest *) + True, + handleDapRequest[msg["command"], msg, newState] + ] +] @@ -745,17 +830,21 @@ cacheResponse[method_String, msg_][state_WorkState] = cacheResponse[method, msg, state] -sendCachedResult[method_String, msg_, state_WorkState] := Block[ +sendCachedResult[method_String, msg_, state_WorkState] := With[ { cache = getCache[method, msg, state] }, - sendResponse[state["client"], - <|"id" -> msg["id"]|> - // Append[ - If[!MissingQ[cache["result"]], - "result" -> cache["result"], - "error" -> cache["error"] + sendMessage[state["client"], + ResponseMessage[<|"id" -> msg["id"]|>] + // ReplaceKey[ + If[MissingQ[cache], + (* File closed, sends Null. *) + "result" -> Null, + If[!MissingQ[cache["result"]], + "result" -> cache["result"], + "error" -> cache["error"] + ] ] ] ]; @@ -774,7 +863,7 @@ scheduleDelayedRequest[method_String, msg_, state_WorkState] := ( ServerConfig["requestDelays"][method], "Second" }], - "id" -> msg["id"], + "id" -> (msg["id"] // Replace[Except[_Integer] -> Missing["NoIdNeeded"]]), "params" -> getScheduleTaskParameter[method, msg, state], "callback" -> (handleRequest[method, msg, #1]&) |>]] @@ -782,9 +871,20 @@ scheduleDelayedRequest[method_String, msg_, state_WorkState] := ( ) -(* response, notification and request will call this function *) -sendResponse[client_, res_Association] := ( - Prepend[res, <|"jsonrpc" -> "2.0"|>] +(* For LSP: response, notification and request *) +sendMessage[client_, res:(_RequestMessage|_ResponseMessage|_NotificationMessage)] := ( + res + // ReplaceKey["jsonrpc" -> "2.0"] + // ToAssociation + // constructRPCBytes + // WriteMessage[client] +) + +(* For DAP: event and response *) +sendMessage[client_, res:(_DapEvent|_DapResponse)] := ( + res + // LogDebug + // ToAssociation // constructRPCBytes // WriteMessage[client] ) @@ -794,27 +894,33 @@ sendResponse[client_, res_Association] := ( (*initialize*) -handleRequest["initialize", msg_, state_WorkState] := Module[ - { - newState = state - }, +handleRequest["initialize", msg_, state_WorkState] := With[ + { + debugPort = Fold[Replace[#1, _?MissingQ -> <||>][#2]&, + msg, {"params", "initializationOptions", "debuggerPort"} + ] + }, - LogDebug["handle/initialize"]; - - (* Check Client Capabilities *) - newState = ReplaceKey[state, - "clientCapabilities" -> msg["params"]["capabilities"] - ]; - - sendResponse[state["client"], <| + sendMessage[state["client"], ResponseMessage[<| "id" -> msg["id"], "result" -> <| "capabilities" -> ServerCapabilities |> - |>]; - - {"Continue", newState} -]; + |>]]; + + (* TODO(kenkangxgwe): check client capabilities *) + { + "Continue", + Fold[ReplaceKey, state, { + "clientCapabilities" -> msg["params"]["capabilities"], + If[!MissingQ[debugPort], + LogInfo["Debugger listening at port " <> ToString[debugPort]]; + {"debugSession", "server"} -> SocketOpen[debugPort], + Nothing + ] + }] + } +] (* ::Subsection:: *) @@ -825,10 +931,10 @@ handleRequest["shutdown", msg_, state_] := Module[ { }, - sendResponse[state["client"], <| + sendMessage[state["client"], ResponseMessage[<| "id" -> msg["id"], "result" -> Null - |>]; + |>]]; {"Continue", state} ]; @@ -844,19 +950,159 @@ handleRequest["workspace/executeCommand", msg_, state_] := With[ args = msg["params"]["arguments"] }, - Replace[command, { - "dap-wl.runfile" -> ( - LogInfo[StringJoin["executing ", command, "with arguments: ", ToString[args]]] - ) - }]; + LogDebug[StringJoin["executing ", command, " with arguments: ", ToString[args]]]; + executeCommand[command, msg, state] +] + + +(* ::Subsubsection:: *) +(*openRef*) + + +executeCommand["openRef", msg_, state_WorkState] := ( + msg["params"]["arguments"] + // First + // SystemOpen + // UsingFrontEnd; + + sendMessage[state["client"], ResponseMessage[<| + "id" -> msg["id"], + "result" -> Null + |>]]; + + {"Continue", state} +) + + +(* ::Subsubsection:: *) +(*dap-wl.evaluate-file*) + + +executeCommand["dap-wl.evaluate-file", msg_, state_WorkState] := With[ + { + args = msg["params"]["arguments"] // First + }, + + sendMessage[state["debugSession"]["client"], DapEvent[<| + "type" -> "event", + "event" -> "continued", + "body" -> <| + "threadId" -> ( + GetThreads[state["debugSession"]["subKernel"]] + // First + // Key["id"] + ) + (* , "allThreadsContinued" -> True *) + |> + |>]]; + + text = state["openedDocs"][args["uri"]] + // GetDocumentText + // StringTrim + // (LogDebug["Evaluating " <> #]; #)&; + + sendMessage[state["debugSession"]["client"], DapEvent[<| + "type" -> "event", + "event" -> "output", + "body" -> <| + (* "category" -> "stdout", *) + "output" -> ( + DebuggerEvaluate[ + <|"expression" -> text|>, + state["debugSession"]["subKernel"] + ] + ) + |> + |>]]; + + sendMessage[state["debugSession"]["client"], DapEvent[<| + "type" -> "event", + "event" -> "stopped", + "body" -> <| + "reason" -> "pause", + "description" -> "Cell Evaluated Successfully", + "threadId" -> ( + GetThreads[state["debugSession"]["subKernel"]] + // First + // Key["id"] + ) + (* , "allThreadsStopped" -> True *) + |> + |>]]; - sendResponse[state["client"], <| + sendMessage[state["client"], ResponseMessage[<| "id" -> msg["id"], "result" -> Null - |>]; + |>]]; {"Continue", state} +] + + +(* ::Subsubsection:: *) +(*dap-wl.evaluate-range*) + + +executeCommand["dap-wl.evaluate-range", msg_, state_WorkState] := Block[ + { + args = msg["params"]["arguments"] // First, + text + }, + + sendMessage[state["debugSession"]["client"], DapEvent[<| + "type" -> "event", + "event" -> "continued", + "body" -> <| + "threadId" -> ( + GetThreads[state["debugSession"]["subKernel"]] + // First + // Key["id"] + ) + (* , "allThreadsContinued" -> True *) + |> + |>]]; + + text = state["openedDocs"][args["uri"]] + // GetDocumentText[#, ConstructType[args["range"], _LspRange]]& + // StringTrim + // (LogDebug["Evaluating " <> #]; #)&; + + sendMessage[state["debugSession"]["client"], DapEvent[<| + "type" -> "event", + "event" -> "output", + "body" -> <| + (* "category" -> "stdout", *) + "output" -> ( + DebuggerEvaluate[ + <|"expression" -> text|>, + state["debugSession"]["subKernel"] + ] + // (# <> "\n")& + ) + |> + |>]]; + + sendMessage[state["debugSession"]["client"], DapEvent[<| + "type" -> "event", + "event" -> "stopped", + "body" -> <| + "reason" -> "pause", + "description" -> "Cell Evaluated Successfully", + "threadId" -> ( + GetThreads[state["debugSession"]["subKernel"]] + // First + // Key["id"] + ) + (* , "allThreadsStopped" -> True *) + |> + |>]]; + + sendMessage[state["client"], ResponseMessage[<| + "id" -> msg["id"], + "result" -> Null + |>]]; + {"Continue", state} ] @@ -865,25 +1111,25 @@ handleRequest["workspace/executeCommand", msg_, state_] := With[ handleRequest["textDocument/publishDiagnostics", uri_String, state_WorkState] := ( - sendResponse[state["client"], <| + sendMessage[state["client"], ResponseMessage[<| "method" -> "textDocument/publishDiagnostics", "params" -> <| "uri" -> uri, - "diagnostics" -> ToAssociation[DiagnoseDoc[state["openedDocs"][uri]]] + "diagnostics" -> DiagnoseDoc[state["openedDocs"][uri]] |> - |>]; + |>]]; {"Continue", state} ) handleRequest["textDocument/clearDiagnostics", uri_String, state_WorkState] := ( - sendResponse[state["client"], <| + sendMessage[state["client"], ResponseMessage[<| "method" -> "textDocument/publishDiagnostics", "params" -> <| "uri" -> uri, "diagnostics" -> {} |> - |>]; + |>]]; {"Continue", state} ) @@ -899,15 +1145,17 @@ getScheduleTaskParameter[method:"textDocument/publishDiagnostics", uri_String, s handleRequest["textDocument/hover", msg_, state_] := With[ { - doc = state["openedDocs"][msg["params"]["textDocument"]["uri"]], + uri = msg["params"]["textDocument"]["uri"], pos = LspPosition[msg["params"]["position"]] }, - sendResponse[state["client"], <| + sendMessage[state["client"], ResponseMessage[<| "id" -> msg["id"], - "result" -> ToAssociation[GetHoverAtPosition[doc, pos]] - |>]; - + "result" -> GetHoverAtPosition[ + state["openedDocs"][uri], + pos + ] + |>]]; {"Continue", state} ] @@ -931,19 +1179,22 @@ cacheResponse[method:"textDocument/signatureHelp", msg_, state_WorkState] := Wit }, state - // ReplaceKey[ - {"caches", method, uri} -> RequestCache[<| - "cachedTime" -> Now, - "result" -> ( - GetSignatureHelp[state["openedDocs"][uri], pos] - // ToAssociation - ) - |>] + // If[MissingQ[state["openedDocs"][uri]], + Identity, + ReplaceKey[ + {"caches", method, uri} -> RequestCache[<| + "cachedTime" -> Now, + "result" -> GetSignatureHelp[ + state["openedDocs"][uri], + pos + ] + |>] + ] ] ] -cacheAvailableQ[method:"textDocument/signatureHelp", msg_, state_WorkState] := Block[ +cacheAvailableQ[method:"textDocument/signatureHelp", msg_, state_WorkState] := With[ { cachedTime = getCache[method, msg, state]["cachedtime"] }, @@ -966,44 +1217,47 @@ getCache[method:"textDocument/signatureHelp", msg_, state_WorkState] := ( handleRequest["textDocument/completion", msg_, state_] := Module[ { - doc = state["openedDocs"][msg["params"]["textDocument"]["uri"]], + uri = msg["params"]["textDocument"]["uri"], pos = LspPosition[msg["params"]["position"]] }, msg["params"]["context"]["triggerKind"] // Replace[{ CompletionTriggerKind["Invoked"] :> ( - sendResponse[state["client"], <| + sendMessage[state["client"], ResponseMessage[<| "id" -> msg["id"], "result" -> <| "isIncomplete" -> False, - "items" -> ToAssociation@GetTokenCompletionAtPostion[doc, pos] + "items" -> GetTokenCompletionAtPostion[ + state["openedDocs"][uri], + pos + ] |> - |>] + |>]] ), CompletionTriggerKind["TriggerCharacter"] :> ( - sendResponse[state["client"], <| + sendMessage[state["client"], ResponseMessage[<| "id" -> msg["id"], "result" -> <| "isIncomplete" -> True, - "items" -> ( - GetTriggerKeyCompletion[doc, pos] - // ToAssociation - ) + "items" -> GetTriggerKeyCompletion[ + state["openedDocs"][uri], + pos + ] |> - |>] + |>]] ), CompletionTriggerKind["TriggerForIncompleteCompletions"] :> ( - sendResponse[state["client"], <| + sendMessage[state["client"], ResponseMessage[<| "id" -> msg["id"], "result" -> <| "isIncomplete" -> False, - "items" -> ( - GetIncompleteCompletionAtPosition[doc, pos] - // ToAssociation - ) + "items" -> GetIncompleteCompletionAtPosition[ + state["openedDocs"][uri], + pos + ] |> - |>]; + |>]]; ) }]; @@ -1030,13 +1284,13 @@ handleRequest["completionItem/resolve", msg_, state_] := With[ msg["params"]["data"]["type"] // Replace[{ "Alias" | "LongName" :> ( - sendResponse[state["client"], <| + sendMessage[state["client"], ResponseMessage[<| "id" -> msg["id"], "result" -> msg["params"] - |>] + |>]] ), "Token" :> ( - sendResponse[state["client"], <| + sendMessage[state["client"], ResponseMessage[<| "id" -> msg["id"], "result" -> <| msg["params"] @@ -1047,7 +1301,7 @@ handleRequest["completionItem/resolve", msg_, state_] := With[ |> ] |> - |>] + |>]] ) }]; @@ -1066,14 +1320,17 @@ handleRequest["completionItem/resolve", msg_, state_] := With[ handleRequest["textDocument/definition", msg_, state_] := With[ { - doc = state["openedDocs"][msg["params"]["textDocument"]["uri"]], + uri = msg["params"]["textDocument"]["uri"], pos = LspPosition[msg["params"]["position"]] }, - sendResponse[state["client"], <| + sendMessage[state["client"], ResponseMessage[<| "id" -> msg["id"], - "result" -> ToAssociation@FindDefinitions[doc, pos] - |>] // AbsoluteTiming // First // LogDebug; + "result" -> FindDefinitions[ + state["openedDocs"][uri], + pos + ] + |>]]; {"Continue", state} ] @@ -1085,15 +1342,19 @@ handleRequest["textDocument/definition", msg_, state_] := With[ handleRequest["textDocument/references", msg_, state_] := With[ { - doc = state["openedDocs"][msg["params"]["textDocument"]["uri"]], + uri = msg["params"]["textDocument"]["uri"], pos = LspPosition[msg["params"]["position"]], includeDeclaration = msg["params"]["context"]["includeDeclaration"] }, - sendResponse[state["client"], <| + sendMessage[state["client"], ResponseMessage[<| "id" -> msg["id"], - "result" -> ToAssociation@FindReferences[doc, pos, "IncludeDeclaration" -> includeDeclaration] - |>] // AbsoluteTiming // First // LogDebug; + "result" -> FindReferences[ + state["openedDocs"][uri], + pos, + "IncludeDeclaration" -> includeDeclaration + ] + |>]]; {"Continue", state} ] @@ -1105,18 +1366,17 @@ handleRequest["textDocument/references", msg_, state_] := With[ handleRequest[method:"textDocument/documentHighlight", msg_, state_WorkState] := With[ { - id = msg["id"], uri = msg["params"]["textDocument"]["uri"], pos = LspPosition[msg["params"]["position"]] }, - sendResponse[state["client"], <| - "id" -> id, - "result" -> ( - FindDocumentHighlight[state["openedDocs"][uri], pos] - // ToAssociation - ) - |>]; + sendMessage[state["client"], ResponseMessage[<| + "id" -> msg["id"], + "result" -> FindDocumentHighlight[ + state["openedDocs"][uri], + pos + ] + |>]]; {"Continue", state} ] @@ -1144,20 +1404,19 @@ cacheResponse[method:"textDocument/documentSymbol", msg_, state_WorkState] := Wi }, state - // ReplaceKey[ - {"caches", method, uri} -> RequestCache[<| - "cachedTime" -> Now, - "result" -> ( - state["openedDocs"][uri] - // ToDocumentSymbol - // ToAssociation - ) - |>] + // If[MissingQ[state["openedDocs"][uri]], + Identity, + ReplaceKey[ + {"caches", method, uri} -> RequestCache[<| + "cachedTime" -> Now, + "result" -> ToDocumentSymbol[state["openedDocs"][uri]] + |>] + ] ] ] -cacheAvailableQ[method:"textDocument/documentSymbol", msg_, state_WorkState] := Block[ +cacheAvailableQ[method:"textDocument/documentSymbol", msg_, state_WorkState] := With[ { cachedTime = getCache[method, msg, state]["cachedtime"] }, @@ -1178,6 +1437,138 @@ getCache[method:"textDocument/documentSymbol", msg_, state_WorkState] := ( ) +(* ::Subsection:: *) +(*textDocument/codeAction*) + + +handleRequest["textDocument/codeAction", msg_, state_] := With[ + { + uri = msg["params"]["textDocument"]["uri"], + range = ConstructType[msg["params"]["range"], LspRange] + }, + + sendMessage[state["client"], ResponseMessage[<| + "id" -> msg["id"], + "result" -> ( + GetCodeActionsInRange[ + state["openedDocs"][uri], + range + ] // If[state["debugSession"]["initialized"], + Append[ + LspCodeAction[<| + "title" -> "Evaluate in Debug Console", + "kind" -> CodeActionKind["Empty"], + "command" -> <| + "title" -> "Evaluate in Debug Console", + "command" -> "dap-wl.evaluate-range", + "arguments" -> {<| + "uri" -> uri, + "range" -> range + |>} + |> + |>] + ], + Identity + ] + ) + |>]]; + + {"Continue", state} +] + + +(* ::Subsection:: *) +(*textDocuent/codeLens*) + + +handleRequest["textDocument/codeLens", msg_, state_] := With[ + { + id = msg["id"], + uri = msg["params"]["textDocument"]["uri"] + }, + + sendMessage[state["client"], ResponseMessage[<| + "id" -> id, + "result" -> ( + If[state["debugSession"]["initialized"], + { + CodeLens[<| + "range" -> <| + "start" -> <| + "line" -> 0, + "character" -> 0 + |>, + "end" -> <| + "line" -> 0, + "character" -> 0 + |> + |>, + "data" -> <| + "title" -> "$(workflow) Evaluate File", + "command" -> "dap-wl.evaluate-file", + "arguments" -> {<| + "uri" -> uri + |>} + |> + |>], + Table[ + CodeLens[<| + (* range can only span one line *) + "range" -> ( + codeRange + // ReplaceKey["end" -> codeRange["start"]] + (* // ReplaceKey[{"end", "line"} -> codeRange["start"]["line"]] + // ReplaceKey[{"end", "character"} -> 1] *) + ), + "data" -> <| + "title" -> "$(play) Evaluate", + "command" -> "dap-wl.evaluate-range", + "arguments" -> {<| + "uri" -> uri, + "range" -> codeRange + |>} + |> + |>], + { + codeRange, + state["openedDocs"][uri] + // FindAllCodeRanges + } + ] + }, + {} + ] + // Flatten + ) + |>]]; + + {"Continue", state} + +] + + +(* ::Subsection:: *) +(*codeLens/resolve*) + + +handleRequest["codeLens/resolve", msg_, state_] := With[ + { + id = msg["id"], + codeLens = msg["params"] + }, + + sendMessage[state["client"], ResponseMessage[<| + "id" -> id, + "result" -> ( + ConstructType[codeLens, CodeLens] + // ReplaceKey[#, "command" -> #["data"]]& + ) + |>]]; + + {"Continue", state} +] + + (* ::Subsection:: *) (*textDocument/documentColor*) @@ -1195,20 +1586,19 @@ cacheResponse[method:"textDocument/documentColor", msg_, state_WorkState] := Wit }, state - // ReplaceKey[ - {"caches", method, uri} -> RequestCache[<| - "cachedTime" -> Now, - "result" -> ( - state["openedDocs"][uri] - // FindDocumentColor - // ToAssociation - ) - |>] + // If[MissingQ[state["openedDocs"][uri]], + Identity, + ReplaceKey[ + {"caches", method, uri} -> RequestCache[<| + "cachedTime" -> Now, + "result" -> FindDocumentColor[state["openedDocs"][uri]] + |>] + ] ] ] -cacheAvailableQ[method:"textDocument/documentColor", msg_, state_WorkState] := Block[ +cacheAvailableQ[method:"textDocument/documentColor", msg_, state_WorkState] := With[ { cachedTime = getCache[method, msg, state]["cachedTime"] }, @@ -1237,17 +1627,14 @@ getScheduleTaskParameter[method:"textDocument/documentColor", msg_, state_WorkSt handleRequest["textDocument/colorPresentation", msg_, state_] := With[ { doc = state["openedDocs"][msg["params"]["textDocument"]["uri"]], - color = msg["params"]["color"] // LspColor, - range = msg["params"]["range"] // LspRange + color = ConstructType[msg["params"]["color"], LspColor], + range = ConstructType[msg["params"]["range"], LspRange] }, - sendResponse[state["client"], <| + sendMessage[state["client"], ResponseMessage[<| "id" -> msg["id"], - "result" -> ( - GetColorPresentation[doc, color, range] - // ToAssociation - ) - |>]; + "result" -> GetColorPresentation[doc, color, range] + |>]]; { "Continue", @@ -1265,14 +1652,14 @@ handleRequest["textDocument/colorPresentation", msg_, state_] := With[ handleRequest[_, msg_, state_] := ( - sendResponse[state["client"], <| + sendMessage[state["client"], ResponseMessage[<| "id" -> msg["id"], "error" -> ServerError["MethodNotFound", msg // ErrorMessageTemplates["MethodNotFound"] // LogError ] - |>]; + |>]]; {"Continue", state} ) @@ -1333,13 +1720,13 @@ handleNotification["$/cancelRequest", msg_, state_] := With[ Part[state["scheduledTasks"], pos]["type"] // StringJoin[#, " request is cancelled."]& // LogDebug; - sendResponse[state["client"], <| + sendMessage[state["client"], ResponseMessage[<| "id" -> id, "error" -> ServerError[ "RequestCancelled", "The request is cancelled." ] - |>]; + |>]]; state // ReplaceKeyBy["scheduledTasks" -> (Delete[pos])] ), @@ -1390,14 +1777,15 @@ handleNotification["textDocument/didClose", msg_, state_] := With[ uri = msg["params"]["textDocument"]["uri"] }, - LogDebug @ ("Close Document " <> uri); + LogInfo @ ("Close Document " <> uri); state // ReplaceKeyBy[{"openedDocs"} -> KeyDrop[uri]] // ReplaceKeyBy["caches" -> (Fold[ReplaceKeyBy, #, { "textDocument/documentSymbol" -> KeyDrop[uri], "textDocument/documentColor" -> KeyDrop[uri], - "textDocument/codeLens" -> KeyDrop[uri] + "textDocument/codeLens" -> KeyDrop[uri], + "textDocument/publishDiagnostics" -> KeyDrop[uri] }]&)] // handleRequest["textDocument/clearDiagnostics", uri, #]& ] @@ -1501,6 +1889,376 @@ handleNotification[_, msg_, state_] := ( ) +(* ::Section:: *) +(*Send Request*) + + +$requestId = 0 +getRequestId[] := ( + "req_" <> ToString[($requestId += 1)] +) + + +(* ::Subsection:: *) +(*workspace/applyEdit*) + + +sendRequest[method:"workspace/applyEdit", msg_, state_WorkState] := With[ + { + id = getRequestId[] + }, + + sendMessage[state["client"], RequestMessage[<| + "id" -> id, + "method" -> method, + "params" -> <| + "edit" -> msg["params"]["edit"] + |> + |>]]; + + { + "Continue", + state + // ReplaceKeyBy["pendingServerRequests" -> Append[id -> method]] + } +] + + +(* ::Subsection:: *) +(*workspace/codeLens/refresh*) + + +sendRequest[method:"workspace/codeLens/refresh", state_WorkState] := With[ + { + id = getRequestId[] + }, + + sendMessage[state["client"], RequestMessage[<| + "id" -> id, + "method" -> method, + "params" -> <||> + |>]]; + + { + "Continue", + state + // ReplaceKeyBy["pendingServerRequests" -> Append[id -> method]] + } +] + + +(* ::Section:: *) +(*Handle Response*) + + +handleResponse[_, msg_, state_WorkState] := ( + { + "Continue", + state + // DeleteKey[{"pendingServerRequests", msg["id"]}] + } +) + + +(* ::Section:: *) +(*Handle Dap Request*) + + +(* ::Subsection:: *) +(*initialize*) + + +handleDapRequest["initialize", msg_, state_WorkState] := Block[ + { + subKernel = CreateDebuggerKernel[] + }, + + LogInfo["Initializing Wolfram Language Debugger"]; + + sendMessage[state["debugSession"]["client"], DapResponse[<| + "type" -> "response", + "request_seq" -> msg["seq"], + "success" -> True, + "command" -> msg["command"], + "body" -> <| + "supportsConfigurationDoneRequest" -> True, + "supportsEvaluateForHovers" -> True + |> + |>]]; + + sendMessage[state["debugSession"]["client"], DapEvent[<| + "type" -> "event", + "event" -> "initialized" + |>]]; + + state + // ReplaceKeyBy["debugSession" -> ( + Fold[ReplaceKey, #, { + "initialized" -> True, + "subKernel" -> subKernel, + "context" -> context, + "thread" -> DapThread[<| + "id" -> GetKernelId[subKernel], + "name" -> StringJoin[ + "SubKernel ", + GetProcessId[subKernel] + // ToString + ] + |>], + "symbolTable" -> <||> + }]&)] + // sendRequest["workspace/codeLens/refresh", #]& +] + + +(* ::Subsection:: *) +(*configurationDone*) + + +handleDapRequest["configurationDone", msg_, state_WorkState] := ( + + LogInfo["Configuration Done for Wolfram Language Debugger"]; + + sendMessage[state["debugSession"]["client"], DapResponse[<| + "type" -> "response", + "request_seq" -> msg["seq"], + "success" -> True, + "command" -> msg["command"], + "body" -> <||> + |>]]; + + sendMessage[state["debugSession"]["client"], DapEvent[<| + "type" -> "event", + "event" -> "stopped", + "body" -> <| + "reason" -> "pause", + "description" -> "Cell Evaluated Successfully", + "threadId" -> ( + GetThreads[state["debugSession"]["subKernel"]] + // First + // Key["id"] + ) + (* , "allThreadsStopped" -> True *) + |> + |>]]; + + {"Continue", state} +) + + +(* ::Subsection:: *) +(*attach*) + + +handleDapRequest["attach", msg_, state_WorkState] := ( + + LogInfo["Attaching to Wolfram Language Kernel"]; + + sendMessage[state["debugSession"]["client"], DapResponse[<| + "type" -> "response", + "request_seq" -> msg["seq"], + "success" -> True, + "command" -> msg["command"], + "body" -> <||> + |>]]; + + sendMessage[state["debugSession"]["client"], DapEvent[<| + "type" -> "event", + "event" -> "process", + "body" -> <| + "name" -> "wolfram.exe", + "systemProcessId" -> state["debugSession"]["thread"]["id"], + "isLocalProcess" -> True, + "startMethod" -> "attach" + |> + |>]]; + + sendMessage[state["debugSession"]["client"], DapEvent[<| + "type" -> "event", + "event" -> "thread", + "body" -> <| + "reason" -> "wolfram.exe", + "threadId" -> state["debugSession"]["thread"]["id"] + |> + |>]]; + + + {"Continue", state} +) + + +(* ::Subsection:: *) +(*disconnect*) + + +handleDapRequest["disconnect", msg_, state_WorkState] := ( + sendMessage[state["debugSession"]["client"], DapResponse[<| + "type" -> "response", + "request_seq" -> msg["seq"], + "success" -> True, + "command" -> msg["command"], + "body" -> <||> + |>]]; + + state + // ReplaceKey[{"debugSession", "initialized"} -> False] + // ReplaceKey[{"debugSession", "client"} -> Null] + // sendRequest["workspace/codeLens/refresh", #]& +) + + +(* ::Subsection:: *) +(*setBreakpoints*) + + +handleDapRequest["setBreakPoints", msg_, state_WorkState] := ( + sendMessage[state["debugSession"]["client"], DapResponse[<| + "type" -> "response", + "request_seq" -> msg["seq"], + "success" -> True, + "command" -> msg["command"], + "body" -> <| + "breakpoints" -> {} + |> + |>]]; + + {"Continue", state} +) + + +(* ::Subsection:: *) +(*threads*) + + +handleDapRequest["threads", msg_, state_WorkState] := ( + sendMessage[state["debugSession"]["client"], DapResponse[<| + "type" -> "response", + "request_seq" -> msg["seq"], + "success" -> True, + "command" -> msg["command"], + "body" -> <| + "threads" -> GetThreads[ + state["debugSession"]["subKernel"] + ] + |> + |>]]; + + {"Continue", state} +) + + +(* ::Subsection:: *) +(*stackTrace*) + + +handleDapRequest["stackTrace", msg_, state_WorkState] := ( + sendMessage[state["debugSession"]["client"], DapResponse[<| + "type" -> "response", + "request_seq" -> msg["seq"], + "success" -> True, + "command" -> msg["command"], + "body" -> <| + "stackFrames" -> GetStackFrames[ + msg["arguments"], + state["debugSession"]["subKernel"] + ] + |> + |>]]; + + {"Continue", state} +) + + +(* ::Subsection:: *) +(*scopes*) + + +handleDapRequest["scopes", msg_, state_WorkState] := ( + sendMessage[state["debugSession"]["client"], DapResponse[<| + "type" -> "response", + "request_seq" -> msg["seq"], + "success" -> True, + "command" -> msg["command"], + "body" -> <| + "scopes" -> GetScopes[ + msg["arguments"], + state["debugSession"]["subKernel"] + ] + |> + |>]]; + + {"Continue", state} +) + + +(* ::Subsection:: *) +(*variables*) + + +handleDapRequest["variables", msg_, state_WorkState] := ( + sendMessage[state["debugSession"]["client"], DapResponse[<| + "type" -> "response", + "request_seq" -> msg["seq"], + "success" -> True, + "command" -> msg["command"], + "body" -> <| + "variables" -> GetVariables[ + msg["arguments"], + state["debugSession"]["subKernel"] + ] + |> + |>]]; + + {"Continue", state} +) + + +(* ::Subsection:: *) +(*evaluate*) + + +handleDapRequest["evaluate", msg_, state_WorkState] := ( + + sendMessage[state["debugSession"]["client"], DapResponse[<| + "type" -> "response", + "request_seq" -> msg["seq"], + "success" -> True, + "command" -> msg["command"], + "body" -> <| + "result" -> DebuggerEvaluate[ + msg["arguments"], + state["debugSession"]["subKernel"] + ], + "variablesReference" -> 0 + |> + |>]]; + + {"Continue", state} + ) + + +(* ::Subsection:: *) +(*Invalid Dap Request*) + + +handleDapRequest[_, msg_, state_] := ( + sendMessage[state["debugSession"]["client"], DapResponse[<| + "type" -> "response", + "request_seq" -> msg["seq"], + "success" -> False, + "command" -> msg["command"], + "body" -> <| + "error" -> <| + "id" -> msg["seq"], + "format" -> ErrorMessageTemplates["DapRequestNotFound"][msg["command"]] + |> + |> + |>]]; + + {"Continue", state} +) + + (* ::Section:: *) (*Send Message*) @@ -1514,33 +2272,29 @@ MessageType = <| |> showMessage[message_String, msgType_String, state_WorkState] := ( - MessageType[msgType] - // Replace[_?MissingQ :> MessageType["Error"]] - // LogDebug - // (type \[Function] - sendResponse[state["client"], <| - "method" -> "window/showMessage", - "params" -> <| - "type" -> type, - "message" -> message - |> - |>] - ) + sendMessage[state["client"], NotificationMessage[<| + "method" -> "window/showMessage", + "params" -> <| + "type" -> ( + MessageType[msgType] + // Replace[_?MissingQ :> MessageType["Error"]] + ), + "message" -> message + |> + |>]] ) logMessage[message_String, msgType_String, state_WorkState] := ( - MessageType[msgType] - // Replace[_?MissingQ :> MessageType["Error"]] - // LogDebug - // (type \[Function] - sendResponse[state["client"], <| - "method" -> "window/logMessage", - "params" -> <| - "type" -> type, - "message" -> message - |> - |>] - ) + sendMessage[state["client"], NotificationMessage[<| + "method" -> "window/logMessage", + "params" -> <| + "type" -> ( + MessageType[msgType] + // Replace[_?MissingQ :> MessageType["Error"]] + ), + "message" -> message + |> + |>]] ) @@ -1548,7 +2302,7 @@ logMessage[message_String, msgType_String, state_WorkState] := ( (*Handle Error*) -ServerError[errorType_String, msg_String] := ( +ServerError[errorType_String, msg_String] := ResponseError[ <| "code" -> ( errorType @@ -1560,11 +2314,12 @@ ServerError[errorType_String, msg_String] := ( ), "message" -> msg |> -) +] ErrorMessageTemplates = <| - "MethodNotFound" -> StringTemplate["The requested method `method` is invalid or not implemented"] + "MethodNotFound" -> StringTemplate["The requested method \"`method`\" is invalid or not implemented"], + "DapRequestNotFound" -> StringTemplate["The request \"`command`\" is invalid or not implemented"] |> @@ -1606,7 +2361,7 @@ doNextScheduledTask[state_WorkState] := ( Pause[0.001]; {"Continue", state} ), - task_ServerTask :> Block[ + task_ServerTask :> With[ { newState = state // ReplaceKeyBy["scheduledTasks" -> Rest] }, @@ -1615,22 +2370,28 @@ doNextScheduledTask[state_WorkState] := ( task["type"] // Replace[{ method:"textDocument/publishDiagnostics" :> ( - newState - // If[DatePlus[ - newState["openedDocs"][task["params"]]["lastUpdate"], - {5, "Second"}] > Now, - (* Reschedule the task *) - scheduleDelayedRequest[method, task["params"], #]&, - ReplaceKey[ - { - "caches", - "textDocument/publishDiagnostics", - task["params"], - "scheduledQ" - } -> False - ] - /* (task["callback"][#, task["params"]]&) - ] + newState["openedDocs"][task["params"]] + // Replace[{ + (* File closed, does nothing *) + _?MissingQ -> {"Continue", newState}, + _?((DatePlus[#["lastUpdate"], {5, "Second"}] > Now)&) :> ( + (* Reschedule the task *) + newState + // scheduleDelayedRequest[method, task["params"], #]& + ), + _ :> ( + newState + // ReplaceKey[ + { + "caches", + "textDocument/publishDiagnostics", + task["params"], + "scheduledQ" + } -> False + ] + // task["callback"][#, task["params"]]& + ) + }] ), "InitialCheck" :> ( newState @@ -1651,17 +2412,17 @@ doNextScheduledTask[state_WorkState] := ( (* if there will not be a same task in the future, do it now *) _?MissingQ :> If[!MissingQ[task["callback"]], (* If the function is time constrained, than the there should not be a lot of lags. *) - (* TimeConstrained[task["callback"][newState, task["params"]], 0.1, sendResponse[state["client"], <|"id" -> task["params"]["id"], "result" -> <||>|>]], *) + (* TimeConstrained[task["callback"][newState, task["params"]], 0.1, sendMessage[state["client"], ResponseMessage[<|"id" -> task["params"]["id"], "result" -> <||>|>]]], *) task["callback"][newState, task["params"]] // AbsoluteTiming // Apply[(LogInfo[{task["type"], #1}];#2)&], - sendResponse[newState["client"], <| + sendMessage[newState["client"], ResponseMessage[<| "id" -> task["id"], "error" -> ServerError[ "InternalError", "There is no callback function for this scheduled task." ] - |>]; + |>]]; {"Continue", newState} ], (* find a recent duplicate request *) @@ -1669,13 +2430,13 @@ doNextScheduledTask[state_WorkState] := ( (* execute fallback function if applicable *) task["duplicateFallback"][newState, task["params"]], (* otherwise, return ContentModified error *) - sendResponse[newState["client"], <| + sendMessage[newState["client"], ResponseMessage[<| "id" -> task["id"], "error" -> ServerError[ "RequestCancelled", "There is a more recent duplicate request." ] - |>]; + |>]]; {"Continue", newState} ] }] @@ -1730,119 +2491,131 @@ defaultConfig = <| (* ::Subsection:: *) -(*check upgrades*) +(*initial checks*) initialCheck[state_WorkState] := ( checkDependencies[state]; - If[ - DateDifference[ - DateObject[state["config"]["configFileConfig"]["lastCheckForUpgrade"]], - Today - ] < ServerConfig["updateCheckInterval"], - logMessage[ - "Upgrade not checked, only a few days after the last check.", - "Log", - state - ], - (* check for upgrade if not checked for more than checkInterval days *) - checkGitRepo[state]; - (* ReplaceKey[state["config"], "lastCheckForUpgrade" -> DateString[Today]] - // saveConfig *) - ]; + checkUpdates[state]; {"Continue", state} ) -checkGitRepo[state_WorkState] := ( - Check[Needs["GitLink`"], - showMessage[ - "The GitLink is not installed to the current Wolfram kernel, please check upgrades via git manually.", - "Info", - state - ]; - Return[] - ] // Quiet; +(* ::Subsubsection:: *) +(*checkDependencies*) + - If[!GitLink`GitRepoQ[WolframLanguageServer`RootDirectory], +If[FindFile["PacletManager`"] // FailureQ, + checkDependencies[state_Workstate] := ( showMessage[ - "Wolfram Language Server is not in a git repository, cannot detect upgrades.", + "The PacletManager is not installed to the current Wolfram kernel, please check dependencies manually.", "Info", state - ]; - Return[] - ]; + ] + ), + Needs["PacletManager`"]; + checkDependencies[state_WorkState] := With[ + { + dependencies = { + {"CodeParser", "1.*"}, + {"CodeInspector", "1.*"} + } + }, + + dependencies + // Select[PacletFind /* MatchQ[{}]] + // Replace[ + missingDeps:Except[{}] :> ( + StringRiffle[missingDeps, ", ", "-"] + // StringTemplate[StringJoin[ + "These dependencies with correct versions need to be installed or upgraded: ``, ", + "otherwise the server may malfunction. ", + "Please see the [Installation](https://github.com/kenkangxgwe/lsp-wl/blob/master/README.md#installation) section for details." + ]] + // showMessage[#, "Warning", state]& + ) + ] + ] +] - With[{repo = GitLink`GitOpen[WolframLanguageServer`RootDirectory]}, - If[GitLink`GitProperties[repo, "HeadBranch"] != "master", + +(* ::Subsubsection:: *) +(*checkUpdates*) + + +Check[ + Needs["GitLink`"]; + checkUpdates[state_WorkState] := ( + (* check for upgrade if not checked for more than checkInterval days *) + If[ + DateDifference[ + DateObject[state["config"]["configFileConfig"]["lastCheckForUpgrade"]], + Today + ] < ServerConfig["updateCheckInterval"], logMessage[ - "Upgrade not checked, the current branch is not 'master'.", + "Upgrade not checked, only a few days after the last check.", "Log", state - ], - GitLink`GitAheadBehind[repo, "master", GitLink`GitUpstreamBranch[repo, "master"]] - // Replace[ - {_, _?Positive} :> ( - showMessage[ - "A new version detected, please close the server and use 'git pull' to upgrade.", - "Info", - state - ] - ) - ] - ] - ]; -) - -If[$VersionNumber >= 12.1, - pacletInstalledQ[{name_String, version_String}] := ( - PacletObject[name -> version] - // FailureQ // Not - ), - pacletInstalledQ[{name_String, version_String}] := ( - PacletManager`PacletInformation[{name, version}] - // MatchQ[{}] // Not - ) -] + ]; + Return[] + (* ReplaceKey[state["config"], "lastCheckForUpgrade" -> DateString[Today]] + // saveConfig *) + ]; -checkDependencies[state_WorkState] := With[ - { - dependencies = { - {"CodeParser", "1.0"}, - {"CodeInspector", "1.0"} - } - }, + If[!GitLink`GitRepoQ[WolframLanguageServer`RootDirectory], + showMessage[ + "Wolfram Language Server is not in a git repository, cannot detect upgrades.", + "Log", + state + ]; + Return[] + ]; - Check[Needs["PacletManager`"], - showMessage[ - "The PacletManager is not installed to the current Wolfram kernel, please check dependencies manually.", + With[{repo = GitLink`GitOpen[WolframLanguageServer`RootDirectory]}, + If[GitLink`GitProperties[repo, "HeadBranch"] != "master", + showMessage[ + "Upgrade not checked, the current branch is not 'master'.", + "Log", + state + ], + GitLink`GitAheadBehind[repo, "master", GitLink`GitUpstreamBranch[repo, "master"]] + // Replace[ + {_, _?Positive} :> ( + showMessage[ + "A new version detected, please close the server and use 'git pull' to upgrade.", + "Info", + state + ] + ) + ] + ] + ]; + ), + checkUpdates[state_WorkState] := ( + (* + GitLink is not a native paclet for Mathematica / Wolfram Engine. + Don't show the message by default. + *) + (* showMessage[ + "The GitLink is not installed to the current Wolfram kernel, please check upgrades via git manually.", "Info", state - ]; - Return[] - ]; + ]; *) + Null + ) +] // Quiet; - dependencies - // Select[pacletInstalledQ /* Not] - // Replace[ - missingDeps:Except[{}] :> ( - StringRiffle[missingDeps, ", ", "-"] - // StringTemplate[StringJoin[ - "These dependencies with correct versions need to be installed or upgraded: ``, ", - "otherwise the server may malfunction. ", - "Please see the [Installation](https://github.com/kenkangxgwe/lsp-wl/blob/master/README.md#installation) section for details." - ]] - // showMessage[#, "Warning", state]& - ) - ] -] + +(* ::Subsection:: *) +(*Constant Functions*) -WLServerVersion[] := WolframLanguageServer`Version; +WLServerVersion[] := WolframLanguageServer`$Version; WLServerDebug[] := Print["This is a debug function."]; + End[]; diff --git a/src/WolframLanguageServer/Specification.wl b/src/WolframLanguageServer/Specification.wl index 5e134d0..9bbf310 100644 --- a/src/WolframLanguageServer/Specification.wl +++ b/src/WolframLanguageServer/Specification.wl @@ -1,37 +1,62 @@ (* ::Package:: *) +(* Copyright 2018 lsp-wl Authors *) +(* SPDX-License-Identifier: MIT *) + + (* Wolfram Language Server Specification *) -(* Author: kenkangxgwe , - huxianglong -*) BeginPackage["WolframLanguageServer`Specification`"] ClearAll[Evaluate[Context[] <> "*"]]; +(* ::Section:: *) +(* Language Server Protocol*) + + +RequestMessage::usage = "is type of RequestMessage interface in LSP." +ResponseMessage::usage = "is type of ResponseMessage interface in LSP." +ResponseError::usage = "is type of RequestError interface in LSP." +NotificationMessage::usage = "is type of Notification interface in LSP." LspPosition::usage = "is type of Position interface in LSP." LspRange::usage = "is type of Range interface in LSP." -LspLocation::usage = "is type of Location Interface in LSP." -TextEdit::usage = "is type of TextEdit Interface in LSP." -TextDocumentItem::usage = "is type of TextDocumentItem in LSP." +Location::usage = "is type of Location interface in LSP." +Command::usage = "is type of Command interface in LSP." +TextEdit::usage = "is type of TextEdit interface in LSP." +TextDocumentItem::usage = "is type of TextDocumentItem interface in LSP." +WorkspaceEdit::usage = "is type of WorkspaceEdit Interface in LSP." +MarkupContent::usage = "is the type of MarkupContent interface in LSP." TextDocumentContentChangeEvent::usage = "is an event describing a change to a text document. If range and rangeLength are omitted \ the new text is considered to be the full content of the document." -MarkupContent::usage = "is the type of MarkupContent interface in LSP." +Diagnostic::usage = "is the type of Diagnostic interface in LSP." +DiagnosticRelatedInformation::usage = "is the type of DiagnosticRelatedInformation interface in LSP." Hover::usage = "is the type of Hover interface in LSP." SignatureHelp::usage = "is the type of SignatureHelp interface in LSP." SignatureInformation::usage = "is the type of SignatureInformation interface in LSP." ParameterInformation::usage = "is the type of ParameterInformation interface in LSP." DocumentSymbol::usage = "is the type of DocumentSymbol interface in LSP." -Diagnostic::usage = "is the type of Diagnostic interface in LSP." -DiagnosticRelatedInformation::usage = "is the type of DiagnosticRelatedInformation interface in LSP." CompletionItem::usage = "is the type of CompletionItem interface in LSP." -Location::usage = "is the type of Location interface in LSP." DocumentHighlight ::usage = "is the type of Location interface in LSP." +LspCodeAction::usage = "is the type of CodeAction interface in LSP." +CodeLens::usage = "is type of CodeLens Interface in LSP." ColorInformation::usage = "is the type of ColorInformation interface in LSP." LspColor::usage = "is the type of Color interface in LSP." ColorPresentation::usage = "is the type of ColorPresentation interface in LSP." + +(* ::Section:: *) +(* Debug Adaptor Protocol*) + + +DapEvent::usage = "is the type of Event interface in DAP." +DapResponse::usage = "is the type of Response interface in DAP." +DapThread::usage = "is the type of Thread interface in DAP." +StackFrame::usage = "is the type of StackFrame interface in DAP." +Scope::usage = "is the type of Scope interface in DAP." +DapVariable::usage = "is the type of Variable interface in DAP." + + (* ::Section:: *) (*Type Aliases*) @@ -42,6 +67,27 @@ DocumentUri = String (* ::Section:: *) (*Enum Type*) +ErrorCodes = <| + (* Defined by JSON RPC *) + "ParseError" -> -32700, + "InvalidRequest" -> -32600, + "MethodNotFound" -> -32601, + "InvalidParams" -> -32602, + "InternalError" -> -32603, + "serverErrorStart" -> -32099, + "serverErrorEnd" -> -32000, + "ServerNotInitialized" -> -32002, + "UnknownErrorCode" -> -32001, + (* Defined by the protocol *) + "RequestCancelled" -> -32800, + "ContentModified" -> -32801 +|> + +MarkupKind = <| + "PlainText" -> "plaintext", + "Markdown" -> "markdown" +|> + TextDocumentSyncKind = <| "None" -> 0, "Full" -> 1, @@ -55,35 +101,16 @@ DiagnosticSeverity = <| "Hint" -> 4 |> - -ErrorCodes = <| - (* Defined by JSON RPC *) - "ParseError" -> -32700, - "InvalidRequest" -> -32600, - "MethodNotFound" -> -32601, - "InvalidParams" -> -32602, - "InternalError" -> -32603, - "serverErrorStart" -> -32099, - "serverErrorEnd" -> -32000, - "ServerNotInitialized" -> -32002, - "UnknownErrorCode" -> -32001, - (* Defined by the protocol *) - "RequestCancelled" -> -32800, - "ContentModified" -> -32801 -|> - -InsertTextFormat = <| - "PlainText" -> 1, - "Snippet" -> 2 -|> - - CompletionTriggerKind = <| "Invoked" -> 1, "TriggerCharacter" -> 2, "TriggerForIncompleteCompletions" -> 3 |> +InsertTextFormat = <| + "PlainText" -> 1, + "Snippet" -> 2 +|> CompletionItemKind = <| "Text" -> 1, @@ -113,7 +140,6 @@ CompletionItemKind = <| "TypeParameter" -> 25 |> - SymbolKind = <| "File" -> 1, "Module" -> 2, @@ -143,18 +169,22 @@ SymbolKind = <| "TypeParameter" -> 26 |> - -MarkupKind = <| - "PlainText" -> "plaintext", - "Markdown" -> "markdown" -|> - DocumentHighlightKind = <| "Text" -> 1, "Read" -> 2, "Write" -> 3 |> +CodeActionKind = <| + "Empty" -> "", + "QuickFix" -> "quickfix", + "Refactor" -> "refactor", + "RefactorExtract" -> "refactor.extract", + "RefactorInline" -> "refactor.inline", + "RefactorRewrite" -> "refactor.rewrite", + "Source" -> "source", + "SourceOrganizeImports" -> "source.orgainizeImports" +|> (* ::Section:: *) (*Constants*) @@ -169,7 +199,45 @@ Needs["DataType`"] (* ::Section:: *) -(*Server Communication Related Type*) +(*Language Server Types*) + + +(* ::Subsection:: *) +(*Basic Protocol*) + + +DeclareType[RequestMessage, <| + "jsonrpc" -> _String, + "id" -> _Integer | _String, + "method" -> _String, + "params" -> _ +|>] + +DeclareType[ResponseMessage, <| + "jsonrpc" -> _String, + "id" -> _Integer | _String, + "result" -> _, + "error" -> _ResponseError +|>] + + +DeclareType[ResponseError, <| + "code" -> _Integer, + "message" -> _String, + "data" -> _ +|>] + + +DeclareType[NotificationMessage, <| + "jsonrpc" -> _String, + "method" -> _String, + "params" -> _ +|>] + + +(* ::Subsection:: *) +(*Basic Structures*) + DeclareType[LspPosition, <| "line" -> _Integer, @@ -181,16 +249,29 @@ DeclareType[LspRange, <| "end" -> _LspPosition |>] -DeclareType[LspLocation, <| +DeclareType[Location, <| "uri" -> _DocumentUri, "range" -> _LspRange |>] +DeclareType[Command, <| + "title" -> _String, + "command" -> _String, + "arguments" -> _List +|>] + DeclareType[TextEdit, <| "range" -> _LspRange, "newText" -> _String |>] +DeclareType[WorkspaceEdit, <| + "changes" -> <| + (_DocumentUri -> TextEdit)... + |>, + "documentChanges" -> _ (* not implemented *) +|>] + DeclareType[TextDocumentItem, <| "uri" -> _DocumentUri, "languageId" -> _String, @@ -198,15 +279,57 @@ DeclareType[TextDocumentItem, <| "text" -> _String |>] +DeclareType[MarkupContent, <| + "kind" -> _?(MemberQ[MarkupKind, #]&), + "value" -> _String +|>] + + +(* ::Subsection:: *) +(*Text Synchronization*) + + DeclareType[TextDocumentContentChangeEvent, <| "range" -> _LspRange, "rangeLength" -> _Integer, "text" -> _String |>] -DeclareType[MarkupContent, <| - "kind" -> _String, - "value" -> _String + +(* ::Subsection:: *) +(*Diagnostics*) + + +DeclareType[Diagnostic, <| + "range" -> _LspRange, + "severity" -> _?(MemberQ[DiagnosticSeverity, #]&), + "code" -> _Integer|_String, + "source" -> _String, + "message" -> _String, + "relatedInformation" -> {___DiagnosticRelatedInformation} +|>] + +DeclareType[DiagnosticRelatedInformation, <| + "location" -> _Location, + "message" -> _String +|>] + + +(* ::Subsection:: *) +(*Language Features*) + + +DeclareType[CompletionItem, <| + "label" -> _String, + "kind" -> _Integer, + "detail" -> _String, + "documentation" -> _String | _MarkupContent, + "preselect" -> _?BooleanQ, + "filterText" -> _String, + "insertText" -> _String, + "insertTextFormat" -> _?(MemberQ[InsertTextFormat, #]&), + "textEdit" -> _TextEdit, + "commitCharacters" -> {___String} |>] DeclareType[Hover, <| @@ -234,49 +357,28 @@ DeclareType[ParameterInformation, <| DeclareType[DocumentSymbol, <| "name" -> _String, "detail" -> _String, - "kind" -> _Integer, + "kind" -> _?(MemberQ[SymbolKind, #]&), "deprecated" -> _?BooleanQ, "range" -> _LspRange, "selectionRange" -> _LspRange, "children" -> {___DocumentSymbol} |>] -DeclareType[Diagnostic, <| +DeclareType[DocumentHighlight, <| "range" -> _LspRange, - "severity" -> _Integer, - "code" -> _Integer|_String, - "source" -> _String, - "message" -> _String, - "relatedInformation" -> {___DiagnosticRelatedInformation} -|>] - -DeclareType[DiagnosticRelatedInformation, <| - "location" -> _LspLocation, - "message" -> _String -|>] - - -DeclareType[CompletionItem, <| - "label" -> _String, - "kind" -> _Integer, - "detail" -> _String, - "documentation" -> _String | _MarkupContent, - "preselect" -> _?BooleanQ, - "filterText" -> _String, - "insertText" -> _String, - "insertTextFormat" -> _Integer, - "textEdit" -> _TextEdit, - "commitCharacters" -> {___String} + "kind" -> _Integer |>] -DeclareType[Location, <| - "uri" -> DocumentUri, - "range" -> _LspRange +DeclareType[LspCodeAction, <| + "title" -> _String, + "kind" -> _?(MemberQ[CodeActionKind, #]&), + "command" -> _Command |>] -DeclareType[DocumentHighlight, <| +DeclareType[CodeLens, <| "range" -> _LspRange, - "kind" -> _Integer + "command" -> _Command, + "data" -> _ |>] DeclareType[ColorInformation, <| @@ -297,6 +399,57 @@ DeclareType[ColorPresentation, <| "additionalTextEdits" -> {__TextEdit} |>] + +(* ::Section:: *) +(*Debug Adaptor Types*) + + +DeclareType[DapEvent, <| + "seq" -> _Integer, + "type" -> "event", + "body" -> _ +|>] + +DeclareType[DapResponse, <| + "seq" -> _Integer, + "type" -> "response", + "request_seq" -> _Integer, + "success" -> _?BooleanQ, + "command" -> _String, + "message" -> _String, + "body" -> _ +|>] + +DeclareType[DapThread, <| + "id" -> _?NumericQ, + "name" -> _String +|>] + +DeclareType[StackFrame, <| + "id" -> _?NumericQ, + "name" -> _String, + "line" -> _Integer, + "column" -> _Integer +|>] + +DeclareType[Scope, <| + "name" -> _String, + "variablesReference" -> _Integer, + "namedVariables" -> _Integer, + "indexedVariables" -> _Integer, + "expensive" -> _?BooleanQ +|>] + +DeclareType[DapVariable, <| + "name" -> _String, + "value" -> _String, + "type" -> _String, + "variablesReference" -> _Integer, + "namedVariables" -> _Integer, + "indexedVariables" -> _Integer +|>] + + End[] diff --git a/src/WolframLanguageServer/TableGenerator.wl b/src/WolframLanguageServer/TableGenerator.wl index 01a087c..85d85ca 100644 --- a/src/WolframLanguageServer/TableGenerator.wl +++ b/src/WolframLanguageServer/TableGenerator.wl @@ -1,5 +1,12 @@ (* ::Package:: *) +(* Copyright 2019 lsp-wl Authors *) +(* SPDX-License-Identifier: MIT *) + + +(* Wolfram Language Server Table Generator *) + + BeginPackage["WolframLanguageServer`TableGenerator`"] ClearAll[Evaluate[Context[] <> "*"]] diff --git a/src/WolframLanguageServer/TextDocument.wl b/src/WolframLanguageServer/TextDocument.wl index 0c7b59f..5904648 100644 --- a/src/WolframLanguageServer/TextDocument.wl +++ b/src/WolframLanguageServer/TextDocument.wl @@ -1,9 +1,10 @@ (* ::Package:: *) +(* Copyright 2019 lsp-wl Authors *) +(* SPDX-License-Identifier: MIT *) + + (* Wolfram Language Server TextDocument *) -(* Author: kenkangxgwe , - huxianglong -*) BeginPackage["WolframLanguageServer`TextDocument`"] @@ -23,6 +24,9 @@ FindDefinitions::usage = "FindDefinitions[doc_TextDocument, pos_LspPosition] giv FindReferences::usage = "FindReferences[doc_TextDocument, pos_LspPosition, o:OptionsPattern[]] gives the references of the symbol at the position." FindDocumentHighlight::usage = "FindDocumentHighlight[doc_TextDocument, pos_LspPosition] gives a list of DocumentHighlight." FindAllCodeRanges::usage = "FindAllCodeRanges[doc_TextDocument] returns a list of LspRange which locate all the code ranges (cells) in the given doc." +GetCodeActionsInRange::usage = "GetCodeActionsInRange[doc_TextDocument, range_LspRange] returns a list of CodeAction related to specified range." +GetDocumentText::usage = "GetDocumentText[doc_TextDocument] returns the text of the whole doc except for the shebang line (if exists).\n\ +GetDocumentText[doc_TextDocument, range_LspRange] returns the text of the doc at given range." FindDocumentColor::usage = "FindDocumentColor[doc_TextDocument] gives a list of colors in the text document." GetColorPresentation::usage = "GetColorPresentation[doc_TextDocument, color_LspColor, range_LspRange] gives the RGBColor presentation of the color." @@ -38,6 +42,25 @@ Needs["WolframLanguageServer`AstPatterns`"] Needs["WolframLanguageServer`ColorTable`"] +(* ::Section:: *) +(*Cache*) + +(* + Idealy, cache is a side-effect that doesn't violate purity and referential + transparency. Thus, monad (or something equivalent) is not needed. + However, as for now, the cache is indexed by the URIs, so an arbitrary + input may results in different output due to the last input. +*) + + +(* $Cell stores the results from divideCells for each uri *) +$Cell = <||> + + +(* $CodeRange stores the key-value pairs of codeRange and its AST for each uri *) +$CodeRange = <||> + + (* ::Section:: *) (*CodeParser Shims*) @@ -70,10 +93,10 @@ DeclareType[TextDocument, <| |>] TextDocument /: ToString[textDocument_TextDocument] := StringJoin["TextDocument[<|", - "\"uri\" -> ", textDocument["uri"], ", ", + "\"uri\" -> ", ToString[textDocument["uri"]], ", ", "\"text\" -> ", textDocument["text"] // Shallow // ToString, ", ", "\"version\" -> ", ToString[textDocument["version"]], ", ", - "\"cell\" -> ", ToString[textDocument["cell"]], + "\"lastUpdate\" -> ", ToString[textDocument["lastUpdate"]], ", ", "|>]"] TextDocument /: Format[textDocument_TextDocument] := ToString[textDocument] @@ -104,6 +127,9 @@ ChangeTextDocument[doc_TextDocument, contextChange_TextDocumentContentChangeEven newtext = StringSplit[contextChange["text"], EOL, All] }, + + KeyDropFrom[$Cell, doc["uri"]]; + KeyDropFrom[$CodeRange, doc["uri"]]; ReplaceKey[doc, "text" -> ( contextChange["range"] // Replace[{ @@ -148,7 +174,7 @@ ChangeTextDocument[doc_TextDocument, contextChange_TextDocumentContentChangeEven (* ::Section:: *) -(*CodeCells*) +(*Helper Function*) (* ::Subsection:: *) @@ -165,51 +191,86 @@ DeclareType[CellNode, <| "children" -> {___CellNode} |>] -divideCells[doc_TextDocument] := ( - Position[ - doc["text"], - (* matches style line *) - _?(StringContainsQ["(* " ~~ "::" ~~ Shortest[style___] ~~ "::" ~~ " *)"]), - {1}, Heads -> False + +Options[divideCells] = { + "CodeRange" -> False +} + +divideCells[doc_TextDocument, o:OptionsPattern[]] := ( + If[$Cell[doc["uri"]] // MissingQ, + LogDebug["NewDivide!"]; + Position[ + doc["text"], + (* matches style line *) + _?(StringContainsQ["(* " ~~ "::" ~~ Shortest[style___] ~~ "::" ~~ " *)"]), + {1}, Heads -> False + ] + // Flatten + // Append[Length[doc["text"]] + 1] + // Prepend[0] + // BlockMap[Apply[constructCellNode[doc, #1, #2]&], #, 2, 1]& + // Fold[InsertCell] + // TerminateCell + // Reap + // MapAt[ + Replace[{codeRange_List} :> codeRange] + /* Catenate + /* (Thread[# -> Missing["NotParsed"]]&) + /* Association, + 2 + ] + // Apply[{cell, codeRange} \[Function] ( + If[doc["uri"] // MissingQ // Not, + (* cache if the URI is not missing *) + AssociateTo[$CodeRange, doc["uri"] -> codeRange]; + AssociateTo[$Cell, doc["uri"] -> cell] + ]; + If[OptionValue["CodeRange"], + {cell, codeRange}, + cell + ] + )], + If[OptionValue["CodeRange"], + {$Cell[doc["uri"]] , $CodeRange[doc["uri"]]}, + $Cell[doc["uri"]] + ] ] - // Flatten - // Append[Length[doc["text"]] + 1] - // Prepend[0] - // BlockMap[Apply[constructCellNode[doc, #1, #2]&], #, 2, 1]& - // Fold[InsertCell] - // TerminateCell - // Replace[err:Except[_CellNode] :> ( - LogError["The result of devideCells is not a CellNode " <> ToString[err]] - )] ) constructCellNode[doc_TextDocument, styleLine_Integer, endLine_Integer] := Block[ { - style, title = Missing["Untitled"], codeStart + style, title, codeStart }, style = If[styleLine == 0, AdditionalStyle["File"], Part[doc["text"], styleLine] - // StringCases["(* "~~"::"~~Shortest[style___]~~"::"~~" *)" :> style] + // StringCases["(* " ~~ "::" ~~ Shortest[style___] ~~ "::" ~~ " *)" :> style] + // First + // StringSplit[#, "::"]& // First // Replace["" -> "[empty]"] ]; - If[!AnonymousStyleQ[style] && + {title, codeStart} = If[!AnonymousStyleQ[style] && (styleLine + 1 != endLine), - (Part[doc["text"], styleLine + 1] - // StringCases[ - StartOfString ~~ (Whitespace | "") ~~ - "(*" ~~ Longest[t___] ~~ "*)" ~~ - (Whitespace | "") ~~ EndOfString :> t - ] - // Replace[ - {t_, ___} :> (title = t) - ]); - codeStart = findCodeLine[doc, styleLine + 2], - codeStart = findCodeLine[doc, styleLine + 1] + { + (Part[doc["text"], styleLine + 1] + // StringCases[ + StartOfString ~~ (Whitespace | "") ~~ + "(*" ~~ Longest[t___] ~~ "*)" ~~ + (Whitespace | "") ~~ EndOfString :> t + ] + // Replace[ + {t_, ___} :> t + ]), + findCodeLine[doc, styleLine + 2] + }, + { + Missing["Untitled"], + findCodeLine[doc, styleLine + 1] + } ]; CellNode[<| @@ -244,10 +305,12 @@ constructCellNode[doc_TextDocument, styleLine_Integer, endLine_Integer] := Block |>] |>] ], - "codeRange" -> If[codeStart < endLine, {{codeStart, endLine - 1}}, {}], + "codeRange" -> If[codeStart < endLine, + {{codeStart, endLine - 1}} // Sow, + {} + ], "children" -> {} |>] - ] findCodeLine[doc_TextDocument, currentLine_Integer] := ( @@ -266,19 +329,20 @@ InsertCell[rootCell_CellNode, nextCell_CellNode] := ( rootCell // ReplaceKeyBy[{"children", -1} -> (InsertCell[#, nextCell]&)], rootCell + // If[Length[rootCell["children"]] > 0, + ReplaceKeyBy[{"children", -1} -> TerminateCell], + Identity + ] // If[nextCell["level"] == Infinity, (* Joins the codeRange with root *) ReplaceKeyBy["codeRange" -> (Join[#, nextCell["codeRange"]]&)], Identity ] - // If[Length[rootCell["children"]] > 0, - ReplaceKeyBy[{"children", -1} -> TerminateCell], - Identity - ] (* appends the new cell in the children list *) // ReplaceKeyBy["children" -> Append[ nextCell - // If[nextCell["level"] == Infinity, + // If[nextCell["level"] == Infinity && + Length[nextCell["codeRange"]] > 0, (* removes codeRange *) ReplaceKey[{"range", -1} -> ( First[First[nextCell["codeRange"]]] - 1 @@ -302,15 +366,11 @@ TerminateCell[rootcell_CellNode] := ( Max[ Last[newRootCell["range"]], newRootCell["children"] - // Replace[{ - {___, lastChild_} :> Last[lastChild["range"]], - _ -> -Infinity - }], + // Last[#, <|"range" -> -Infinity|>]& + // Key["range"], newRootCell["codeRange"] - // Replace[{ - {___, {_, last_}} :> last, - _ -> -Infinity - }] + // Last[#, {-Infinity}]& + // Last ] )] )) @@ -334,10 +394,68 @@ HeadingLevel = <| ScriptFileQ[uri_String] := URLParse[uri, "Path"] // Last // FileExtension // EqualTo["wls"] -CellToAST[doc_TextDocument, {startLine_, endLine_}] := ( +(* ::Subsection:: *) +(*Code Range*) + + +getCodeRanges[doc_TextDocument] := ( + divideCells[doc, "CodeRange" -> True] + // Last +) + + +rangeToAst[doc_TextDocument, All] := ( + doc + // { + Identity, + getCodeRanges + /* Keys + } + // Through + // Apply[rangeToAst] + // Catenate +) + + +rangeToAst[doc_TextDocument, range:{_Integer, _Integer}] := rangeToAst[doc, {range}] +rangeToAst[doc_TextDocument, ranges:{{_Integer, _Integer}...}] := With[ + { + uri = doc["uri"] + }, + + ranges + // If[doc["uri"] // MissingQ, + Identity, + (* If cached, get missing ranges only *) + Extract[ + doc + // getCodeRanges + // Lookup[ranges] + // Position[#, _?MissingQ, {1}]& + ] + ] + // Rule[ + Identity, + Map[rangeToCode[doc, #]&] + /* (CodeParser`CodeParse[#, "TabWidth" -> 1]&) + /* (Part[#, All, 2]&) + ] + // Through + // Thread + // If[doc["uri"] // MissingQ, + Values, + Replace[{} -> <||>] + /* (AssociateTo[$CodeRange[uri], #]&) + /* Lookup[uri] + /* Lookup[ranges] + ] + ] + + +rangeToCode[doc_TextDocument, {startLine_Integer, endLine_Integer}] := ( If[startLine == 1 && (doc["text"] // First // StringStartsQ["#!"]), - Return[CellToAST[doc, {2, endLine}]] + Return[rangeToCode[doc, {2, endLine}]] ]; Take[doc["text"], {startLine, endLine}] @@ -349,14 +467,11 @@ CellToAST[doc_TextDocument, {startLine_, endLine_}] := ( {StringRepeat::intp (* before 12.0 *)} ] // Quiet, #]& - // CodeParser`CodeParse - // Part[#, 2]& ) -CellContainsLine[indexLine_Integer][cell_CellNode] := ( - indexLine // Between[cell["range"]] -) +(* ::Subsection:: *) +(*GetAtPosition*) GetCodeRangeAtPosition[doc_TextDocument, pos_LspPosition] := With[ @@ -364,28 +479,80 @@ GetCodeRangeAtPosition[doc_TextDocument, pos_LspPosition] := With[ line = pos["line"] + 1 }, - FirstCase[ - doc // divideCells, - cell_CellNode?(CellContainsLine[line]) :> cell["codeRange"], - {}, {0, Infinity} - ] + doc + // getCodeRanges + // Keys // SelectFirst[Between[line, #]&] ] -FindAllCodeRanges[doc_TextDocument] := ( +GetTokenAtPosition[doc_TextDocument, pos_LspPosition] := ( + GetCodeRangeAtPosition[doc, pos] + // Replace[{ + codeRange: {startLine_Integer, _Integer} :> ( + Take[doc["text"], codeRange] + // StringRiffle[#, "\n"]& + // CodeParser`CodeTokenize + // SelectFirst[NodeContainsPosition[{ + (pos["line"] + 1) - startLine + 1, + pos["character"] + }]] + ) + }] +) - Cases[ - divideCells[doc], - node_CellNode :> node["codeRange"], - {0, Infinity} - ] - // Catenate + +GetAstAtPosition[doc_TextDocument, pos_LspPosition] := ( + GetCodeRangeAtPosition[doc, pos] + // Replace[_?MissingQ -> {}] + // rangeToAst[doc, #]& +) + + +GetSymbolAtPosition[doc_TextDocument, pos_LspPosition] := With[ + { + line = pos["line"] + 1, character = pos["character"] + 1 + }, + + GetAstAtPosition[doc, pos] + // FirstCase[ + #, + AstPattern["Symbol"][symbolName_] + ?(NodeContainsPosition[{line, character}]) :> ( + symbolName + ), + Missing["NotFound"], + AstLevelspec["LeafNodeWithSource"] + ]& +] + + +FindAllCodeRanges[doc_TextDocument] := ( + doc + // getCodeRanges + // Keys // Map[ToLspRange[doc, #]&] ) +GetDocumentText[doc_TextDocument] := ( + doc["text"] + // Replace[{_String?(StringStartsQ["#!"]), restLines___} :> ({"", restLines})] + // StringRiffle[#, "\n"]& +) -(* ::Section:: *) +GetDocumentText[doc_TextDocument, range_LspRange] := ( + doc["text"] + // Take[#, { + range["start"]["line"] + 1, + range["end"]["line"] + 1 + }]& + // MapAt[StringTake[#, range["end"]["character"]]&, -1] + // MapAt[StringDrop[#, range["start"]["character"]]&, 1] + // StringRiffle[#, "\n"]& +) + + +(* ::Subsection:: *) (*AST utils*) @@ -437,6 +604,68 @@ SourceToRange[{{startLine_, startCol_}, {endLine_, endCol_}}] := ( ) +(* ::Section:: *) +(*GetFunctionName*) + + +GetFunctionName[doc_TextDocument, pos_LspPosition] := With[ + { + line = pos["line"] + 1, character = pos["character"] + 1 + }, + + GetAstAtPosition[doc, pos] + // (ast \[Function] ( + FirstPosition[ + ast, + _Association?(NodeDataContainsPosition[{line, character}]), + Missing["NotFound", {}], + AstLevelspec["DataWithSource"], + Heads -> False + ] + // Most + // Replace[indices_List :> ( + getFunctionNameImpl[ast, indices] + )] + )) +] + +getFunctionNameImpl[ast_, indices_] := ( + Extract[ast, indices // Replace[{} -> {All}]] + // Replace[{ + AstPattern["Function"][functionName_] :> ( + functionName + // Replace[FunctionPattern["NoSignatureHelp"] -> Missing["NotFound"]] + ), + _ :> ( + indices + // Replace[{ + {} -> Missing["NotFound"], + _ :> ( + getFunctionNameImpl[ast, indices // Most] + ) + }] + ) + }] +) + + +(* ::Section:: *) +(*GetTokenPrefix*) + + +GetTokenPrefix[doc_TextDocument, pos_LspPosition] := ( + GetTokenAtPosition[doc, pos] + // Replace[{ + AstPattern["Token"][tokenString_, data_] :> ( + StringTake[tokenString, pos["character"] - Part[data[CodeParser`Source], 1, 2] + 1] + ), + (* this happens when line is not in codeRange or character == 0 *) + _?MissingQ -> "", + err_ :> (LogError["Unknown token node " <> ToString[err]]; "") + }] +) + + (* ::Section:: *) (*documentSymbol*) @@ -449,54 +678,33 @@ ToDocumentSymbol[doc_TextDocument] := ( ) -ToDocumentSymbolImpl[doc_TextDocument, node_] := ( - node - // Replace[{ - _CellNode?(Key["style"] /* AnonymousStyleQ /* Not) :> ( - DocumentSymbol[<| - "name" -> node["name"], - "detail" -> node["style"], - "kind" -> If[node["style"] == "Package", - SymbolKind["Package"], - SymbolKind["String"] - ], - "range" -> ToLspRange[doc, node["range"]], - "selectionRange" -> node["selectionRange"], - "children" -> ( - Join[ - If[!MissingQ[node["codeRange"]], - node["codeRange"] - // Map[CellToAST[doc ,#]&] - // Flatten - // Map[ToDocumentSymbolImpl], - {} - ], - If[!MissingQ[node["children"]], - node["children"] - // Map[ToDocumentSymbolImpl[doc, #]&], - {} - ] - ] // Flatten - ) - |>] - ), - _CellNode :> ( - Join[ - If[!MissingQ[node["codeRange"]], - node["codeRange"] - // Map[CellToAST[doc, #]&] - // Flatten - // Map[ToDocumentSymbolImpl], - {} - ], - If[!MissingQ[node["children"]], - node["children"] - // Map[ToDocumentSymbolImpl[doc, #]&], - {} - ] - ] // Flatten - ) - }] +ToDocumentSymbolImpl[doc_TextDocument, node_CellNode] := ( + Join[ + node["codeRange"] + // Replace[_?MissingQ -> {}] + // rangeToAst[doc, #]& + // Flatten + // Map[ToDocumentSymbolImpl], + node["children"] + // Replace[_?MissingQ -> {}] + // Map[ToDocumentSymbolImpl[doc, #]&] + ] + // Flatten + // If[!AnonymousStyleQ[node["style"]], + DocumentSymbol[<| + "name" -> node["name"], + "detail" -> node["style"], + "kind" -> If[node["style"] == "Package", + (* This shouldn't be reachable if "Package" is an anonymous style. *) + SymbolKind["Package"], + SymbolKind["String"] + ], + "range" -> ToLspRange[doc, node["range"]], + "selectionRange" -> node["selectionRange"], + "children" -> # + |>]&, + Identity + ] ) ToDocumentSymbolImpl[node_] := ( @@ -619,6 +827,7 @@ ToDocumentSymbolImpl[node_] := ( ) +(* Convert the line range of the given document to LSP Range. *) ToLspRange[doc_TextDocument, {startLine_Integer, endLine_Integer}] := LspRange[<| "start" -> LspPosition[<| "line" -> startLine - 1, @@ -639,8 +848,9 @@ ToLspRange[doc_TextDocument, {startLine_Integer, endLine_Integer}] := LspRange[< |>] -GetSymbolList[node_] := ( - node +(* Get all the symbols in the specified nested list AST node. *) +GetSymbolList[nestedList_] := ( + nestedList // Replace[{ AstPattern["Function"][functionName:"List", arguments_] :> ( arguments @@ -665,40 +875,33 @@ GetHoverInfo[doc_TextDocument, pos_LspPosition] := With[ line = pos["line"] + 1, character = pos["character"] + 1 }, - GetCodeRangeAtPosition[doc, pos] - // Replace[lineRange:{_Integer, _Integer} :> ( - CellToAST[doc, lineRange] - // (ast \[Function] ( - FirstPosition[ - ast, - _Association?(NodeDataContainsPosition[{line, character}]), - Missing["NotFound", {(* Will be Discarded by Most *)}], - AstLevelspec["DataWithSource"], - Heads -> False - ] - // Most - // Replace[indices_List :> { - getHoverInfoImpl[ast, indices] - // Reap - // Last // Flatten - // DeleteDuplicates, - (* get range *) - ast - // Extract[indices] - // Last - // Key[CodeParser`Source] - // Replace[{ - _?MissingQ -> Nothing, - source_ :> SourceToRange[source] - }] + GetAstAtPosition[doc, pos] + // (ast \[Function] ( + FirstPosition[ + ast, + _Association?(NodeDataContainsPosition[{line, character}]), + {{(* Will be Discarded by Most *)}}, + AstLevelspec["DataWithSource"], + Heads -> False + ] + // Most + // (indices \[Function] { + getHoverInfoImpl[ast, indices] + // Reap + // Last // Flatten + // DeleteDuplicates, + (* get range *) + ast + // Extract[indices] + // Replace[{} -> {<||>}] + // Last + // Key[CodeParser`Source] + // Replace[{ + _?MissingQ -> Nothing, + source_ :> SourceToRange[source] }] - )) - )] - // Replace[ - (* This happens when line not in codeRange or position not in node *) - _?MissingQ :> {{(* empty hover text: *)} (*, no range *)} - ] - + }) + )) ] @@ -739,99 +942,18 @@ getHoverInfoImpl[ast_, {index_Integer, restIndices___}] := ( ) -(* ::Section:: *) -(*GetFunctionName*) - - -GetFunctionName[doc_TextDocument, pos_LspPosition] := With[ - { - line = pos["line"] + 1, character = pos["character"] + 1 - }, - - GetCodeRangeAtPosition[doc, pos] - // Replace[lineRange:{_Integer, _Integer} :> ( - CellToAST[doc, lineRange] - // (ast \[Function] ( - FirstPosition[ - ast, - _Association?(NodeDataContainsPosition[{line, character}]), - Missing["NotFound", {}], - AstLevelspec["DataWithSource"], - Heads -> False - ] - // Most - // Replace[indices_List :> ( - getFunctionNameImpl[ast, indices] - )] - )) - )] -] - -getFunctionNameImpl[ast_, indices_] := ( - Extract[ast, indices // Replace[{} -> {All}]] - // Replace[{ - AstPattern["Function"][functionName_] :> ( - functionName - // Replace[FunctionPattern["NoSignatureHelp"] -> Missing["NotFound"]] - ), - _ :> ( - indices - // Replace[{ - {} -> Missing["NotFound"], - _ :> ( - getFunctionNameImpl[ast, indices // Most] - ) - }] - ) - }] -) - - -(* ::Section:: *) -(*GetTokenPrefix*) - - -GetTokenPrefix[doc_TextDocument, pos_LspPosition] := With[ - { - line = pos["line"] + 1 - }, - - GetCodeRangeAtPosition[doc, pos] - // Replace[lineRange:{rangeStartLine_Integer, _Integer} :> ( - (* get token list *) - Take[doc["text"], lineRange] - // StringRiffle[#, "\n"]& - // CodeParser`CodeTokenize - // SelectFirst[NodeContainsPosition[{ - line - rangeStartLine + 1, - pos["character"] - }]] - // Replace[{ - AstPattern["Token"][tokenString_, data_] :> ( - StringTake[tokenString, pos["character"] - Part[data[CodeParser`Source], 1, 2] + 1] - ), - err_ :> (LogError["Unknown token node " <> ToString[err]]; "") - }] - )] // Replace[ - (* this happens when line is not in codeRange or character == 0 *) - _?MissingQ -> "" - ] -] - - (* ::Section:: *) (*Diagnostics*) DiagnoseDoc[doc_TextDocument] := ( - doc["text"] - // Replace[{_String?(StringStartsQ["#!"]), restLines___} :> ({"", restLines})] - // StringRiffle[#, "\n"]& + doc + // GetDocumentText // Replace[err:Except[_String] :> (LogError[doc]; "")] - // CodeInspector`CodeInspect + // CodeInspector`CodeInspect[#, "TabWidth" -> 1]& // Replace[_?FailureQ -> {}] - // ReplaceAll[CodeInspector`InspectionObject[tag_, description_, severity_, data_] :> Diagnostic[<| + // Cases[CodeInspector`InspectionObject[tag:Except["BadSymbol"], description_, severity_, data_] :> Diagnostic[<| "range" -> ( data // Key[CodeParser`Source] @@ -845,17 +967,17 @@ DiagnoseDoc[doc_TextDocument] := ( severity // Replace[{ "Fatal" -> "Error", + "Error" -> "Warning", + "Warning" -> "Information", "Formatting"|"Remark" -> "Hint" }] - // Replace[{ - "Warning" :> ( - tag - // Replace[{ - "ExperimentalSymbol" -> "Hint", - _ -> "Warning" - }] - ) - }] + // (newSeverity \[Function] ( + tag + // Replace[{ + "ExperimentalSymbol" -> "Hint", + _ -> newSeverity + }] + )) // DiagnosticSeverity ), "source" -> "Wolfram", @@ -866,7 +988,7 @@ DiagnoseDoc[doc_TextDocument] := ( (* // ReplaceAll[{CodeInspector`Format`LintMarkup[content_, ___] :> ( ToString[content] )}] *) - // StringReplace["``" -> "\""] + // StringReplace["``"|"**" -> "\""] ] ) |>]] @@ -947,86 +1069,81 @@ Options[FindScopeOccurence] = { "BodySearch" -> True } -FindScopeOccurence[doc_TextDocument, pos_LspPosition, o:OptionsPattern[]] := Block[ +FindScopeOccurence[doc_TextDocument, pos_LspPosition, o:OptionsPattern[]] := With[ { - line = pos["line"] + 1, character = pos["character"] + 1, - ast, name + line = pos["line"] + 1, character = pos["character"] + 1 }, - ast = GetCodeRangeAtPosition[doc, pos] - // Replace[lineRange:{_Integer, _Integer} :> ( - CellToAST[doc, lineRange] - )] - // Replace[_?MissingQ :> Return[{{}, {}}]]; - - name = FirstCase[ - ast, - AstPattern["Symbol"][symbolName_] - ?(NodeContainsPosition[{line, character}]) :> ( - symbolName - ), - Missing["NotFound"], - AstLevelspec["LeafNodeWithSource"] - ] - // Replace[_?MissingQ :> Return[{{}, {}}]]; - - LogDebug["Searching for " <> name]; - - FirstCase[ - ast, - ( - AstPattern["Scope"][head_, body_, op_] - ?(NodeContainsPosition[{line, character}]) | - AstPattern["Delayed"][head_, body_, op_] - ?(NodeContainsPosition[{line, character}]) - ) :> Block[ + GetSymbolAtPosition[doc, pos] + // (LogDebug["FindScopeOccurence: " <> ToString[#]];#)& + // Replace[{ + name_String :> Block[ { - headSource + ast = GetAstAtPosition[doc, pos] }, - { - headSource, - If[OptionValue["BodySearch"], - Replace[op, { - FunctionPattern["StaticLocal"] :> - StaticLocalSource[body, name], - FunctionPattern["DynamicLocal"] :> - DynamicLocalSource[body, name] - }], - {} - ] - } - (* a pattern test with inner side effect *) - /; ( - Replace[op, { - FunctionPattern["Scope"] :> - ScopeHeadSymbolSource[op, head, name], - FunctionPattern["Delayed"] :> - DelayedHeadPatternNameSource[head, name] - }] - // ((headSource = #)&) - // MatchQ[Except[{}, _List]] - ) - ], - (* search it the whole doc as a dynamic local *) - { - {}, - OptionValue["GlobalSearch"] - // Replace[{ - True :> DynamicLocalSource[ - CellToAST[doc, {1, doc["text"] // Length}], - name + LogDebug["Searching for " <> name]; + FirstCase[ + ast, + ( + AstPattern["Scope"][head_, body_, op_] + ?(NodeContainsPosition[{line, character}]) | + AstPattern["Delayed"][head_, body_, op_] + ?(NodeContainsPosition[{line, character}]) + ) :> Block[ + { + headSource + }, + + { + headSource, + If[OptionValue["BodySearch"], + Replace[op, { + FunctionPattern["StaticLocal"] :> + StaticLocalSource[body, name], + FunctionPattern["DynamicLocal"] :> + DynamicLocalSource[body, name] + }], + {} + ] + } + (* a pattern test with inner side effect *) + /; ( + Replace[op, { + FunctionPattern["Scope"] :> + ScopeHeadSymbolSource[op, head, name], + FunctionPattern["Delayed"] :> + DelayedHeadPatternNameSource[head, name] + }] + // ((headSource = #)&) + // MatchQ[Except[{}, _List]] + ) ], - "TopLevelOnly" :> ( - CellToAST[doc, {1, doc["text"] // Length}] - // Map[FindTopLevelSymbols[#, name]&] - // Catenate - ), - _ -> {} - }] - }, - {0, Infinity} - ] + (* search it the whole doc as a dynamic local *) + ast = rangeToAst[doc, All]; + OptionValue["GlobalSearch"] + // Replace[{ + True :> ( + { + {}, + DynamicLocalSource[ast, name] + } + ), + "TopLevelOnly" :> ( + { + {}, + ast + // Map[FindTopLevelSymbols[#, name]&] + // Catenate + } + ), + _ -> {{}, {}} + }], + {0, Infinity} + ] + ], + _?MissingQ :> {{}, {}} + }] ] @@ -1247,46 +1364,73 @@ FindTopLevelSymbols[node_, name_String] := ( ) -(* ::Subsection:: *) -(*DocumentColor*) - +(* ::Section:: *) +(*CodeAction*) + + +$referencePageCache = <||> + +hasReferencePage[symbol_String] := ( + If[$referencePageCache // KeyMemberQ[symbol], + $referencePageCache[symbol], + $referencePageCache[symbol] = + FindFile[FileNameJoin[{"ReferencePages", "Symbols", symbol <> ".nb"}]] + // If[!FailureQ[#] && + (* FindFile is case-insensitive on Windows. Needs AbsoluteFileName to confirm. *) + (!$OperatingSystem == "Windows" || AbsoluteFileName[#] == #), + #, + Missing["NotFound"] + ]& + ] +) -FindDocumentColor[doc_TextDocument] := With[ +GetCodeActionsInRange[doc_TextDocument, range_LspRange] := With[ { - ast = CellToAST[doc, {1, doc["text"] // Length}] + startPos = {range["start"]["line"] + 1, range["start"]["character"] + 1}, + endPos = {range["end"]["line"] + 1, range["end"]["character"]} }, - Join[ - Cases[ - ast, - AstPattern["NamedColor"][color_, data_] :> ( - ColorInformation[<| - "range" -> ( - data - // Key[CodeParser`Source] - // SourceToRange - ), - "color" -> ( - ColorConvert[ToExpression[color], "RGB"] - // Apply[List] - // ToLspColor - ) + GetAstAtPosition[doc, range["start"]] + // FirstCase[ + #, + AstPattern["Token"][tokenString_]?(( + (* The token node overlaps the range *) + CompareNodePosition[#, startPos, -1] >= 0 && + CompareNodePosition[#, endPos, 1] <= 0 + )&) :> ( + hasReferencePage[tokenString] + // Replace[referencePath_?(MissingQ /* Not) :> ( + LspCodeAction[<| + "title" -> "Documentation: " <> tokenString, + "kind" -> CodeActionKind["Empty"], + "command" -> <| + "title" -> "Documentation: " <> tokenString, + "command" -> "openRef", + "arguments" -> {referencePath} + |> |>] - ), - AstLevelspec["LeafNodeWithSource"] - ], - Cases[ - ast, - AstPattern["ColorModel"][model_, params_, data_] :> With[ - { - color = ( - params - // Map[CodeParser`FromNode] - // Apply[ToExpression[model]] - ) - }, + )] + ), + Missing["NotFound"], + AstLevelspec["DataWithSource"], + Heads -> False + ]& + // List + // DeleteMissing +] + - If[ColorQ[color], +(* ::Section:: *) +(*DocumentColor*) + + +FindDocumentColor[doc_TextDocument] := ( + rangeToAst[doc, All] + // (ast \[Function] ( + Join[ + Cases[ + ast, + AstPattern["NamedColor"][color_, data_] :> ( ColorInformation[<| "range" -> ( data @@ -1294,18 +1438,46 @@ FindDocumentColor[doc_TextDocument] := With[ // SourceToRange ), "color" -> ( - ColorConvert[color, "RGB"] + ColorConvert[ToExpression[color], "RGB"] // Apply[List] // ToLspColor ) - |>], - Nothing - ] + |>] + ), + AstLevelspec["LeafNodeWithSource"] ], - AstLevelspec["CallNodeWithArgs"] + Cases[ + ast, + AstPattern["ColorModel"][model_, params_, data_] :> With[ + { + color = ( + params + // Map[CodeParser`FromNode] + // Apply[ToExpression[model]] + ) + }, + + If[ColorQ[color], + ColorInformation[<| + "range" -> ( + data + // Key[CodeParser`Source] + // SourceToRange + ), + "color" -> ( + ColorConvert[color, "RGB"] + // Apply[List] + // ToLspColor + ) + |>], + Nothing + ] + ], + AstLevelspec["CallNodeWithArgs"] + ] ] - ] -] + )) +) GetColorPresentation[doc_TextDocument, color_LspColor, range_LspRange] := With[ diff --git a/src/WolframLanguageServer/Token.wl b/src/WolframLanguageServer/Token.wl index 3b2e153..8d14f40 100644 --- a/src/WolframLanguageServer/Token.wl +++ b/src/WolframLanguageServer/Token.wl @@ -1,16 +1,17 @@ (* ::Package:: *) +(* Copyright 2019 lsp-wl Authors *) +(* SPDX-License-Identifier: MIT *) + + (* Wolfram Language Server Token *) -(* Author: kenkangxgwe , - huxianglong -*) BeginPackage["WolframLanguageServer`Token`"] ClearAll[Evaluate[Context[] <> "*"]] -TokenDocumentation::usage = "TokenDocumentation[token_String, tag_String, o] returns the documentation for input token in Markdown format. +TokenDocumentation::usage = "TokenDocumentation[token_String, tag_String, o] returns the documentation for input token in specified format. The possible options are \"Format\" -> \"plaintext\" | \"markdown\" " @@ -164,7 +165,7 @@ GenHeader[token_String, tag_String, o: OptionsPattern[]] := ( } // Through // Apply[ If[OptionValue["Format"] == MarkupKind["Markdown"], - StringTemplate["**`1`** `2` (`3`)\n"], + StringTemplate["**`1`** `2` `3`\n"], StringTemplate["`1`\t(`3`)\n"] ] ] @@ -204,7 +205,8 @@ Options[GenAttributes] = { GenAttributes[token_String, o:OptionsPattern[]] := ( Attributes[token] // Replace[_Attributes -> {}] - // StringRiffle[#, ", "]& + // StringRiffle[#, {"(", ", ", ")"}]& + // Replace["()" -> ""] ) Options[GenOptions] = { @@ -214,7 +216,8 @@ GenOptions[token_String, o:OptionsPattern[]] := ( token // StringTemplate["Options[``]"] // ToExpression - // Replace[_Options -> {}] + // Quiet + // Replace[_Options|_?FailureQ -> {}] // Map[ToString[#, InputForm]&] // Replace[{options__} :> ( If[OptionValue["Format"] == MarkupKind["Markdown"], @@ -509,7 +512,6 @@ GetTokenCompletionAtPostion[doc_TextDocument, pos_LspPosition] := With[ ] -(* SetDelayed is not needed. Cache it when define it. *) GetTriggerKeyCompletion[doc_TextDocument, pos_LspPosition] := ( If[GetTokenPrefix[doc, pos] == "\\\\", (* double-triggered *) @@ -519,6 +521,7 @@ GetTriggerKeyCompletion[doc_TextDocument, pos_LspPosition] := ( ) +(* SetDelayed is not needed. Cache it when define it. *) NonLetterAliasCompletionItems = ( Join[ AliasToLongName diff --git a/test/PatternTemplateTest.wl b/test/PatternTemplateTest.wl index 9518659..4d27b4c 100644 --- a/test/PatternTemplateTest.wl +++ b/test/PatternTemplateTest.wl @@ -1,5 +1,12 @@ (* ::Package:: *) +(* Copyright 2019 lsp-wl Authors *) +(* SPDX-License-Identifier: MIT *) + + +(* Wolfram Language Server Pattern Template Test *) + + BeginPackage["PatternTemplateTest`"] ClearAll[Evaluate[Context[] <> "*"]] diff --git a/test/RunTest.wl b/test/RunTest.wl index 1de0b08..6ba2fca 100644 --- a/test/RunTest.wl +++ b/test/RunTest.wl @@ -1,5 +1,12 @@ (* ::Package:: *) +(* Copyright 2019 lsp-wl Authors *) +(* SPDX-License-Identifier: MIT *) + + +(* Wolfram Language Server Test Runner *) + + BeginPackage["RunTest`"] ClearAll[Evaluate[Context[] <> "*"]] diff --git a/test/WolframLanguageServer/TextDocumentTest.wl b/test/WolframLanguageServer/TextDocumentTest.wl index a6fc74c..8dbdcd7 100644 --- a/test/WolframLanguageServer/TextDocumentTest.wl +++ b/test/WolframLanguageServer/TextDocumentTest.wl @@ -1,5 +1,12 @@ (* ::Package:: *) +(* Copyright 2019 lsp-wl Authors *) +(* SPDX-License-Identifier: MIT *) + + +(* Wolfram Language Server TextDocument Test *) + + BeginPackage["WolframLanguageServer`TextDocumentTest`"] ClearAll[Evaluate[Context[] <> "*"]] @@ -104,6 +111,148 @@ VerificationTest[ TestID -> "ChangeTextDocument2" ], +VerificationTest[ + ToDocumentSymbol[TextDocument[<| + "text" -> { + "(* " ~~ "::Section::" ~~ " *)", + "(*section name*)", + "", + "", + "(* " ~~ "::nostyle::" ~~ " *)", + "(*section name*)", + "", + "" + } + |>]], + { + DocumentSymbol[<| + "name" -> "section name", + "detail" -> "Section", + "kind" -> 15, + "range" -> LspRange[<| + "start" -> LspPosition[<| + "line" -> 0, + "character" -> 0 + |>], + "end" -> LspPosition[<| + "line" -> 7, + "character" -> 0 + |>] + |>], + "selectionRange" -> LspRange[<| + "start" -> LspPosition[<| + "line" -> 1, + "character" -> 2 + |>], + "end" -> LspPosition[<| + "line" -> 1, + "character" -> 14 + |>] + |>], + "children" -> { + DocumentSymbol[<| + "name" -> "section name", + "detail" -> "nostyle", + "kind" -> 15, + "range" -> LspRange[<| + "start" -> LspPosition[<| + "line" -> 4, + "character" -> 0 + |>], + "end" -> LspPosition[<| + "line" -> 7, + "character" -> 0 + |>] + |>], + "selectionRange" -> LspRange[<| + "start" -> LspPosition[<| + "line" -> 5, + "character" -> 2 + |>], + "end" -> LspPosition[<| + "line" -> 5, + "character" -> 14 + |>] + |>], + "children"->{} + |>] + } + |>] + }, + TestID -> "ToDocumentSymbolEmptySymbol1" +], + +VerificationTest[ + ToDocumentSymbol[TextDocument[<| + "text" -> { + "(* " ~~ "::Section::" ~~ " *)", + "(*section name*)", + "", + "", + "(* " ~~ "::Subsection::Closed::" ~~ " *)", + "(*section name*)", + "", + "" + } + |>]], + { + DocumentSymbol[<| + "name" -> "section name", + "detail" -> "Section", + "kind" -> 15, + "range" -> LspRange[<| + "start" -> LspPosition[<| + "line" -> 0, + "character" -> 0 + |>], + "end" -> LspPosition[<| + "line" -> 7, + "character" -> 0 + |>] + |>], + "selectionRange" -> LspRange[<| + "start" -> LspPosition[<| + "line" -> 1, + "character" -> 2 + |>], + "end" -> LspPosition[<| + "line" -> 1, + "character" -> 14 + |>] + |>], + "children" -> { + DocumentSymbol[<| + "name" -> "section name", + "detail" -> "Subsection", + "kind" -> 15, + "range" -> LspRange[<| + "start" -> LspPosition[<| + "line" -> 4, + "character" -> 0 + |>], + "end" -> LspPosition[<| + "line" -> 7, + "character" -> 0 + |>] + |>], + "selectionRange" -> LspRange[<| + "start" -> LspPosition[<| + "line" -> 5, + "character" -> 2 + |>], + "end" -> LspPosition[<| + "line" -> 5, + "character" -> 14 + |>] + |>], + "children"->{} + |>] + } + |>] + }, + TestID -> "ToDocumentSymbolCompoundStyle1" +], + VerificationTest[ FindAllCodeRanges[TextDocument[<| "text" -> { @@ -121,7 +270,7 @@ VerificationTest[ "character" -> 30 |>] |>]}, - TestID -> "FindAllCodeRangePackage1" + TestID -> "FindAllCodeRangesPackage1" ], VerificationTest[ @@ -142,7 +291,7 @@ VerificationTest[ "character" -> 30 |>] |>]}, - TestID -> "FindAllCodeRangePackage2" + TestID -> "FindAllCodeRangesPackage2" ], VerificationTest[ @@ -172,7 +321,7 @@ VerificationTest[ |>] |>] }, - TestID -> "FindAllCodeRangeSection1" + TestID -> "FindAllCodeRangesSection1" ], VerificationTest[ @@ -200,7 +349,7 @@ VerificationTest[ |>] |>] }, - TestID -> "FindAllCodeRangeSection2" + TestID -> "FindAllCodeRangesSection2" ], VerificationTest[ @@ -226,7 +375,7 @@ VerificationTest[ |>] |>] }, - TestID -> "FindAllCodeRangeSection3" + TestID -> "FindAllCodeRangesSection3" ], VerificationTest[ @@ -255,7 +404,52 @@ VerificationTest[ |>] |>] }, - TestID -> "FindAllCodeRangeTwoSection1" + TestID -> "FindAllCodeRangesTwoSection1" +], + +VerificationTest[ + FindAllCodeRanges[TextDocument[<| + "text" -> { + "(* " ~~ "::UnknownStyle::" ~~ " *)", + "(*style title*)", + "", + "", + "(* code range with one line *)", + "(* " ~~ "::UnknownStyle::" ~~ " *)", + "(*style title*)", + "", + "", + "(* code range with two lines *)", + "(* code range with two lines *)", + "(* " ~~ "::UnknownStyle::" ~~ " *)", + "(*style title*)", + "", + "" + } + |>]], + { + LspRange[<| + "start" -> LspPosition[<| + "line" -> 4, + "character" -> 0 + |>], + "end" -> LspPosition[<| + "line" -> 5, + "character" -> 0 + |>] + |>], + LspRange[<| + "start" -> LspPosition[<| + "line" -> 9, + "character" -> 0 + |>], + "end" -> LspPosition[<| + "line" -> 11, + "character" -> 0 + |>] + |>] + }, + TestID -> "FindAllCodeRangesMultipleUnknownStyles1" ], VerificationTest[ @@ -471,6 +665,22 @@ VerificationTest[ |>] }, TestID -> "HoverOperator 2" +], + +VerificationTest[ + GetHoverInfo[ + TextDocument[<| + "text" -> { + "(* this is comment *)" + } + |>], + LspPosition[<| + "line" -> 0, + "character" -> 2 + |>] + ], + {{}}, + TestID -> "HoverComment 1" ] } // Map[Sow[#, CurrentContext]&] diff --git a/test/WolframLanguageServer/TokenTest.wl b/test/WolframLanguageServer/TokenTest.wl index 6c4f350..9d3920a 100644 --- a/test/WolframLanguageServer/TokenTest.wl +++ b/test/WolframLanguageServer/TokenTest.wl @@ -1,5 +1,12 @@ (* ::Package:: *) +(* Copyright 2019 lsp-wl Authors *) +(* SPDX-License-Identifier: MIT *) + + +(* Wolfram Language Server Token Test *) + + BeginPackage["WolframLanguageServer`TokenTest`"] ClearAll[Evaluate[Context[] <> "*"]] @@ -21,7 +28,7 @@ Needs["WolframLanguageServer`TextDocument`"] VerificationTest[ TokenDocumentation["BeginPackage", "usage"], StringJoin[ - "**BeginPackage** [*reference*](https://reference.wolfram.com/language/ref/BeginPackage.html) (Protected)\n\n\n", + "**BeginPackage** [*reference*](https://reference.wolfram.com/language/ref/BeginPackage.html) (Protected)\n\n\n", "```mathematica\n", "BeginPackage[\"context`\"]\n", "```\n\n", @@ -37,7 +44,7 @@ VerificationTest[ VerificationTest[ TokenDocumentation["Replace", "usage"], StringJoin[ - "**Replace** [*reference*](https://reference.wolfram.com/language/ref/Replace.html) (Protected)\n\n\n", + "**Replace** [*reference*](https://reference.wolfram.com/language/ref/Replace.html) (Protected)\n\n\n", "```mathematica\n", "Replace[expr,rules]\n", "```\n\n", @@ -62,7 +69,7 @@ If[$VersionNumber >= 12.0, VerificationTest[ TokenDocumentation["SlotSequence", "usage"], StringJoin[ - "**SlotSequence** [*reference*](https://reference.wolfram.com/language/ref/SlotSequence.html) (NHoldAll, Protected)\n\n\n", + "**SlotSequence** [*reference*](https://reference.wolfram.com/language/ref/SlotSequence.html) (NHoldAll, Protected)\n\n\n", "```mathematica\n", "## \n", "```\n\n", @@ -82,6 +89,21 @@ If[$VersionNumber >= 12.0, ] ], +If[$FrontEnd === Null, +VerificationTest[ + TokenDocumentation["$FrontEndSession", "usage"], + StringJoin[ + "**$FrontEndSession** [*reference*](https://reference.wolfram.com/language/ref/$FrontEndSession.html) (Protected, ReadProtected)\n\n\n", + "```mathematica\n", + "$FrontEndSession \n", + "```\n\n", + "is a global symbol that represents the current session of the front end from which the kernel is being run.\n\n" + ], + TestID -> "KnownSymbolUsage 4" +], +Nothing +], + VerificationTest[ TokenDocumentation["Syntax", "stresc"], StringJoin[