Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lsp for JavaScript/TypeScript #15

Closed
ArkadiuszMichalski opened this issue Aug 7, 2024 · 12 comments
Closed

Lsp for JavaScript/TypeScript #15

ArkadiuszMichalski opened this issue Aug 7, 2024 · 12 comments
Labels
question Further information is requested

Comments

@ArkadiuszMichalski
Copy link

After some short testing Lsp for C/C++ I started looking something for JavaScript. From what I have seen the only solution is:
typescript-language-server which is a bridge for tsserver (something from Microsoft used in their tools, unfortunately it still does not support LSP directly microsoft/TypeScript#39459).

How to install?

  1. If you already have Node available in Path and want to have these tools also globally, then call:
    npm install -g typescript-language-server typescript
    Then config for Lsp should looks like:
[lspservers."javascript"]
mode = "io"
args = '--stdio'
executable = 'typescript-language-server.cmd'
auto_start_server = false
  1. For people who don't have/don't use Node and want to check it quickly locally:
    2.1 Download Node (with NPM) from here https://nodejs.org/dist/latest/ (.7z or .zip).
    2.2 Unpack it for example to Node folder.
    2.3 Inside Node folder run:
    npm install -g typescript-language-server typescript

Inside Node and Node\node_modules new files should appear.
image
Then config for Lsp should looks like:

[lspservers."javascript"]
mode = "io"
args = '--stdio'
executable = 'Node\typescript-language-server.cmd'
auto_start_server = false

Of course, Node is a virtual name, it should be the full path to this folder, like:

D:\Node
D:\somefolder\Node
C:\tools\Node

etc., depending on where you unpacked Node.

After a quick tests, I can see that the most important commands work:

  • Goto definition
  • Find references
  • Symbol windows
  • Format document and Format selected text

Not work:

  • Rename << but the same I have for C/C++
  • Ask for help << it shows something there and we can select, but after that it does not perform any operation.

Poorly work:

  • the popup with the hint works, but when we click on the link it always opens a tiny window, it doesn't remember the last size.
    image

Nice, I didn't think I'd see contextual jumping in Notepad++ for JavaScript (tools based on ctags can't do that). Great job, I hope you keep developing it, in Notepad++ we miss these new solutions from other editors when working on code (with larger code they become more and more necessary).

@Ekopalypse
Copy link
Owner

Thank you very much for taking the time to document how a node based server can be used.
I will, with your permission, work it into the tips and tricks section.

With Rename and Ask for help, which is basically lsp actions, there are variations of the returned structures
which are not yet fully implemented. But having servers that use them makes it easier to implement them.

I enjoy programming my plugins, but unfortunately the time I can spend on it is very limited at the moment.
I hope that I can work on it for 4-5 hours a day during my vacation then it should be a whole step closer to be a fully working lsp client.

@ArkadiuszMichalski
Copy link
Author

Thank you very much for taking the time to document how a node based server can be used.
I will, with your permission, work it into the tips and tricks section.

No problem, I prefer to discuss here rather than on the forum, because it's always closer to the plugin. It's easier to search/read such discussions and tips later.

I enjoy programming my plugins, but unfortunately the time I can spend on it is very limited at the moment.
I hope that I can work on it for 4-5 hours a day during my vacation then it should be a whole step closer to be a fully working lsp client.

There is no rush, it's not easy either, from what I see each LSP server has different coverage (and other differences, like configuration) so in the future it will probably be necessary to fine-tune everything a bit for specific ones (at least the most popular ones that the Notepad++ community would prefer).

If I ask a lot of questions here or open a new issue, there is no need for a quick response, I just write down what I can observe so that I don't forget about it later. When you have a moment, you can come back to the topic.

At the moment I have one important question, can you tell me if the LSP protocol supports any filtration for the folders/files it operates on? What I mean is that if we have a Project folder, can we somehow attach a folder from another location to it, like SomeLibrary? And the opposite situation, can we exclude some other folder from the Project folder, e.g. modules? In short:

Somelibrary << include this folder
Project << this is root of project
    modules << exclude this folder
    ...

If the LSP protocol itself does not provide this, then we will probably need to analyze each server separately and check its options.

@Ekopalypse
Copy link
Owner

I prefer to discuss here

I also think it makes more sense to discuss plugin related stuff here.

in the future it will probably be necessary to fine-tune

Yes, this is necessary because it is possible to have custom lsp messages per server,
which I currently ignore and this will probably not be available for the first officially released version.
First the basic functionality has to work reliably.

can you tell me if the LSP protocol supports any filtration for the folders/files it operates on?

Yes, Workspace folders.

can we exclude some other folder

but no, if you mean that such a folder is a subdirectory of the project root directory.

Currently, the content of the FoldersAsWorkspaces(FAW) dialog is used to inform the servers about the directories in use.
When the buffer is activated, I check whether the content has changed and send the notification Workspace/didChangeWorkspaceFolders accordingly.
This means that only FAWs root nodes are taken into account.

The problem I have with FAW is that it is a performance bottleneck with language servers that compile sources, as it monitors and updates the directories. Also, it would be really helpful if I could reuse a dialog like this to show a diagnostic counter next to the folders, so you can see immediately that there are problems without having to open the diagnostic dialog. So I'm thinking about giving the lsp client its own "Projects/FAW" dialog.

@ArkadiuszMichalski
Copy link
Author

@Ekopalypse But how all roots in FAW are treated, as the same project or separate? I mean will the suggestions, searches etc. work separately for each root or together?

In the case of tsserver we can control on which files it operates by using the config file. I tried this and indeed the files property works:

{
  "[files]": [
    "file1.js",
    "file2.js",
  ]
}

Unfortunately, filtering by using include/excludedoesn't work for me. It's possible that I'm doing something wrong.

In general, some additional plugin configuration through a file placed in the project folder would be useful for those who don't use FAW (because it's an additional panel, and we not always have much space on the screen). But I'll open a separate issue for this problem.

@Ekopalypse
Copy link
Owner

But how all roots in FAW are treated

How the root directories are interpreted by the respective servers is beyond the client's knowledge.
The client sends the information about which root directory(s) are available in the initialize request.
This takes place in 3 places, either via

  • workspaceFolders or, if this is not used, via
  • rootUri or if this is not used via
  • rootPath

Now it is up to the respective server to use this knowledge to search for the corresponding language-specific configuration files, if available/used. The rust-analyzer would now search for a cargo.toml in one of these directories, clangd would probably look for the compile_commands.json etc.

But the current way, using FAW, also has the disadvantage that a root directory, which for example only makes sense for the typescript server, would also be made known to clangd. Unfortunately, I don't see how this can be solved easily.

In general, some additional plugin configuration through a file ...

But how is this supposed to work? If we take the following project as an example,

root
 - sub_dir
  - sub_sub_dir
   - file1
   - file2
 - sub_dir2
  - sub_sub_dir2
   - file3
   - file4

If file4 is now opened, how does the LspClient know where to find this configuration file?
It could of course go from sub_sub_dir2 upwards and search for it, but what if sub_dir2 was already an independent project?
In Rust, for example, it is common for it to look like this

root
 - sub_dir
  - sub_sub_dir
   - file1.rs
   - file2.rs
  lib.rs
  cargo.toml
 - sub_dir2
  - sub_sub_dir2
   - file3.rs
   - file4.rs
  lib.rs
  cargo.toml
 cargo.toml

This means that rust-analyzer only needs the cargo.tml from sub_dir2 to be able to use file4.rs.
Of course, this makes compiling much faster than if it were to compile the complete project.

It would be conceivable, at least for servers that use such configuration files, to define them in the NppLspClient.toml file. So that the client knows that for the rust-analyzer a cargo.toml would have to be searched for, but even that will not be 100% satisfactory, that it can ALSO be that in the above example a cargo.toml exists in the sub_dir2 directory but the cargo.toml from the root directory is still required for the compilation to be successful.

@Ekopalypse Ekopalypse added the question Further information is requested label Aug 10, 2024
@ArkadiuszMichalski
Copy link
Author

ArkadiuszMichalski commented Aug 11, 2024

@Ekopalypse I will open a new issue for this separate config file because it's probably a bigger problem.

Returning to JS I got what I asked for. I had to use jsconfig.json instead of tsconfig.json file with below content:

{
    "include": [
        "**/*.js",
	"D:/outerjs/test3.js"
    ],
    "exclude": [
        "innerjs/*"
    ],
}

We still can use tsconfig.json file but something needs to be added:

{
    "include": [
        "**/*.js",
	"D:/outerjs/test3.js"
    ],
    "exclude": [
        "innerjs/*"
    ],
    "compilerOptions": {
        "allowJs": true,
    }
}

Thanks to this I can point to files that should be analyzed (even outside the root), and add some exclusions inside the root. That's what I cared about the most.

I don't need to open/activate all nested files for LSP commands to work, just one from root is enough. The only problem is when I start the server from a file nested in root a little deeper, it still somehow can't find things above, I have to activate some file directly from root. At the moment it's acceptable, unless I find a solution.

If anyone uses JS/TS let me know what the problem is. In short, when we run the server on a file that is 1 or more level nested below the root, it does not search for things above unless we go to a file that is directly in the root. jsconfig.json or tsconfig.json is located in root. It looks like the search for these config files is not going up (manual).

Edit: This LSP doesn't support workspaceFolders so I don't know if this doesn't that cause my problem.

@Ekopalypse
Copy link
Owner

The only problem is when I start the server from a file nested in root a little deeper

FAW is currently required at this point. The user must have specified the root directory of the project. If you open a file that cannot be found in one of the root directories of FAW, the file directory is used in the initialization request.

@ArkadiuszMichalski
Copy link
Author

ArkadiuszMichalski commented Aug 11, 2024

@Ekopalypse

The user must have specified the root directory of the project.

Some configuration (separate file/command or something else for discussion) would be useful for things like this.

Regardless of this, I noticed one interesting thing for JS:

dirA
    jsconfig.json
    fileA1.js
    dirA2
        fileA2.js

dirB
    fileB1.js
    fileB2.js
  1. When fileA2.js is active and we run server then it't not recognized command from fileA1.js (we need switch to this file, then it will probably process the file jsconfig.json). So in this case it doesn't loop over all parent folders of fileA2.js.

  2. When fileB2.js is active and we run server then command from fileB1.js is recognized (that's interesting because if we did opposite, the commands from fileB2.js would not be available in fileB1.js until we activated file fileB2.js). Well, this seems to be the default behavior within the folder dirB. << it looks like i was wrong here (i probably had a config file defined that's why it worked), without such config file we need to activate a given file so that its stuff is recognized in other files.

    But that's not the point, what's more interesting is that if now we switch to fileA2.js it read jsconfig.json because all the commands from above folders are recognized inside file fileA2.js. So in this case it loop over all parent folders of fileA2.js. It's strange, why wasn't this done in the first case? Was the initialization done differently or is something else important here? I thought it would just stick on dirA2 (like in first case).

    It's a bit confusing because sometimes it iterates up through folders and other times it doesn't. It's difficult to use configuration files in such a situation (we must remember about such variations).

@Ekopalypse
Copy link
Owner

You ran this test with dirA and dirB as root folders in FAW?
If the server supports workspace folders, I would have thought it would recognize the jsconfig.json file and know what to do.
Do you have a simple demo I can play with?

@ArkadiuszMichalski
Copy link
Author

ArkadiuszMichalski commented Aug 12, 2024

You ran this test with dirA and dirB as root folders in FAW?

No, I run it without FAW, this LSP doesn't support FAW. But it's more specific because it supports single file mode.

It seems to me that when we run it for the first time (without FAW) you set root to the current folder and server will then read the config file from that folder if it exists (but it doesn't try to go higher if it doesn't exist). Which would correspond to this description from the manual:

By invoking tsc with no input files and a --project (or just -p) command line option that specifies the path of a directory containing a tsconfig.json file, or a path to a valid .json file containing the configurations.

Then, when we switch to some other file from a folder higher up (or a completely separate folder), it probably treats it as if there was no root specified, and then it looks for the configuration file iterating upwards. Which would correspond to this description from the manual:

By invoking tsc with no input files, in which case the compiler searches for the tsconfig.json file starting in the current directory and continuing up the parent directory chain.

What's interesting is that when we have separate folders with configuration files, it internally treats them as separate projects, it doesn't get lost in navigation through definitions, etc., everything worked as if I had separate projects, or at least that's what I think, because I only tested it on simple functions/variables.

Do you have a simple demo I can play with?

If you want to play with it I will create a folder with such cases in various subfolders. I'll just mention that I don't use commands like import/require etc. for modules. I just treat files as if they lived in one scope and all of them should have access to everything (as if it was one file). Because when we use module import it processes them immediately and everything below is recognized (then even jsconfig.json is unnecessary here).

Next infos:
https://code.visualstudio.com/docs/languages/jsconfig

@Ekopalypse
Copy link
Owner

Ekopalypse commented Aug 14, 2024

No, I run it without FAW, this LSP ...

Just to be clear: FAW is not directly related to the workspace folder feature of the lsp protocol. Although I also use it for this purpose, the other main reason to use it is that a main project root directory can be determined by the lsp client. So the problem with opening fileA2.js and trying to see functions from both fileA1.js and fileB1.js or fileB2.js should not have occurred. (As long as the jsconfig.json has been setup correctly).

To open fileB*.js and see definitions from fileA*.js, in this scenario, it would need the support of the workspace folders from the server.

@Ekopalypse
Copy link
Owner

@ArkadiuszMichalski - reported renaming and code action issues should be fixed in the released version v.0.0.27-alpha.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants