diff --git a/.github/workflows/job.test.yml b/.github/workflows/job.test.yml index dc43df385..cec7859d3 100644 --- a/.github/workflows/job.test.yml +++ b/.github/workflows/job.test.yml @@ -214,15 +214,16 @@ jobs: include: # if using 3.6, use an old node - python: 3.6 - # Node 10 end-of-life: April 2021 - nodejs: '>=10,<11.0.0.a0' - # if using 3.7, use newer node, etc... - - python: 3.7 # Node 12 end-of-life: April 2022 nodejs: '>=12,<13.0.0.a0' - - python: 3.8 + # if using 3.7, use newer node, etc... + - python: 3.7 # Node 14 end-of-life: April 2023 nodejs: '>=14,<15.0.0.a0' + - python: 3.8 + # TODO: switch to Node 16 once gets merged https://github.com/conda-forge/nodejs-feedstock/pull/189 + # Node 15 end-of-life: June 2021 + nodejs: '>=15,<16.0.0.a0' # TODO: remove when mambaforge just works on setup-miniconda - os: ubuntu mambaforge: Linux-x86_64.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d545d56e..71825ec5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,17 @@ - add ability to deactivate Kernel completions or LSP completion through the settings ([#586], thanks @Carreau) - allow to set a priority for LSP server, allowing to choose which server to use when multiple servers are installed ([#588]) + - add auto-detection of pyright server ([#587], thanks @yuntan) - bug fixes: - - workaround url-parse issue causing problems when using JupyterLab 3.0.15 [#599] + + - workaround url-parse issue causing problems when using JupyterLab 3.0.15 ([#599]) + +- other changes: + - drop Node 10 (EOL 2 weeks ago) testing on CI, add Node 15 ([#587]) [#586]: https://github.com/krassowski/jupyterlab-lsp/pull/586 +[#587]: https://github.com/krassowski/jupyterlab-lsp/pull/587 [#588]: https://github.com/krassowski/jupyterlab-lsp/pull/588 [#599]: https://github.com/krassowski/jupyterlab-lsp/pull/599 diff --git a/atest/01_Editor.robot b/atest/01_Editor.robot index 9d2b5ca44..e27ce7650 100644 --- a/atest/01_Editor.robot +++ b/atest/01_Editor.robot @@ -43,9 +43,13 @@ Less Markdown Editor Shows Features for Language Markdown example.md Diagnostics=`Color` is misspelt -Python +Python (pylsp) ${def} = Set Variable xpath:(//span[contains(@class, 'cm-variable')][contains(text(), 'fib')])[last()] - Editor Shows Features for Language Python example.py Diagnostics=multiple spaces after keyword Jump to Definition=${def} Rename=${def} + Editor Shows Features for Server pylsp Python example.py Diagnostics=undefined name 'result' (pyflakes) Jump to Definition=${def} Rename=${def} + +Python (pyright) + ${def} = Set Variable xpath:(//span[contains(@class, 'cm-variable')][contains(text(), 'fib')])[last()] + Editor Shows Features for Server pyright Python example.py Diagnostics=is not defined (Pyright) Jump to Definition=${def} R ${def} = Set Variable xpath:(//span[contains(@class, 'cm-variable')][contains(text(), 'fib')])[last()] @@ -70,6 +74,12 @@ YAML Editor Shows Features for Language YAML example.yaml Diagnostics=duplicate key *** Keywords *** +Editor Shows Features for Server + [Arguments] ${server} ${Language} ${file} &{features} + Configure JupyterLab Plugin + ... {"language_servers": {"${server}": {"priority": 10000}}} + Editor Shows Features for Language ${Language} ${file} &{features} + Editor Shows Features for Language [Arguments] ${Language} ${file} &{features} Prepare File for Editing ${Language} editor ${file} diff --git a/atest/Keywords.robot b/atest/Keywords.robot index f6dcfcbb3..a58e011f5 100644 --- a/atest/Keywords.robot +++ b/atest/Keywords.robot @@ -375,6 +375,7 @@ Configure JupyterLab Plugin Set Editor Content ${settings json} ${CSS USER SETTINGS} Wait Until Page Contains No errors found Click Element css:button[title\='Save User Settings'] + Wait Until Page Contains No errors found Click Element ${JLAB XP CLOSE SETTINGS} Clean Up After Working with File and Settings diff --git a/atest/examples/example.py b/atest/examples/example.py index 43860557a..65ef1a758 100644 --- a/atest/examples/example.py +++ b/atest/examples/example.py @@ -2,7 +2,6 @@ from itertools import (accumulate, chain); accumulate - # fibs :: Integer :: [Integer] def fibs(n): '''An accumulation of the first n integers in @@ -27,5 +26,6 @@ def go(ab, _): print( 'First twenty: ' + repr( fibs(20) - ) + ), + result ) diff --git a/package.json b/package.json index 2363b00ab..9d389a5bb 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "npm-run-all": "^4.1.5", "precise-commits": "^1.0.2", "prettier": "^2.1.2", + "pyright": "^1.1", "sql-language-server": "^0.11.4", "typescript": "~4.1.3", "unified-language-server": "^0.3.0", diff --git a/packages/jupyterlab-lsp/schema/plugin.json b/packages/jupyterlab-lsp/schema/plugin.json index 8893348fa..ce314702b 100644 --- a/packages/jupyterlab-lsp/schema/plugin.json +++ b/packages/jupyterlab-lsp/schema/plugin.json @@ -31,7 +31,13 @@ "language_servers": { "title": "Language Server", "description": "Language-server specific configuration, keyed by implementation, e.g: \n\npyls: {\n serverSettings: {\n pyls: {\n plugins: {\n pydocstyle: {\n enabled: true\n },\n pyflakes: {\n enabled: false\n },\n flake8: {\n enabled: true\n }\n }\n }\n }\n}\n\nAlternatively, using VSCode's naming convention:\n\npyls: {\n serverSettings: {\n \"pyls.plugins.pydocstyle.enabled\": true,\n \"pyls.plugins.pyflakes.enabled\": false,\n \"pyls.plugins.flake8.enabled\": true\n }\n}", - "default": {}, + "default": { + "pyright": { + "serverSettings": { + "python.analysis.useLibraryCodeForTypes": true + } + } + }, "patternProperties": { ".*": { "$ref": "#/definitions/language-server" diff --git a/packages/jupyterlab-lsp/src/connection_manager.ts b/packages/jupyterlab-lsp/src/connection_manager.ts index 1893ec5fb..926aa3bdd 100644 --- a/packages/jupyterlab-lsp/src/connection_manager.ts +++ b/packages/jupyterlab-lsp/src/connection_manager.ts @@ -162,16 +162,28 @@ export class DocumentConnectionManager { } /** - * Currently only supports the settings that the language servers - * accept using onDidChangeConfiguration messages, under the - * "serverSettings" keyword in the setting registry. New keywords can - * be added and extra functionality implemented here when needed. + * Handles the settings that do not require an existing connection + * with a language server (or can influence to which server the + * connection will be created, e.g. `priority`). + * + * This function should be called **before** initialization of servers. + */ + public updateConfiguration(allServerSettings: TLanguageServerConfigurations) { + this.language_server_manager.setConfiguration(allServerSettings); + } + + /** + * Handles the settings that the language servers accept using + * `onDidChangeConfiguration` messages, which should be passed under + * the "serverSettings" keyword in the setting registry. + * Other configuration options are handled by `updateConfiguration` instead. + * + * This function should be called **after** initialization of servers. */ public updateServerConfigurations( allServerSettings: TLanguageServerConfigurations ) { let language_server_id: TServerKeys; - this.language_server_manager.setConfiguration(allServerSettings); for (language_server_id in allServerSettings) { if (!allServerSettings.hasOwnProperty(language_server_id)) { diff --git a/packages/jupyterlab-lsp/src/index.ts b/packages/jupyterlab-lsp/src/index.ts index 80de10f02..b8a71e7ba 100644 --- a/packages/jupyterlab-lsp/src/index.ts +++ b/packages/jupyterlab-lsp/src/index.ts @@ -179,8 +179,11 @@ export class LSPExtension implements ILSPExtension { .then(settings => { // Store the initial server settings, to be sent asynchronously // when the servers are initialized. - this.connection_manager.initial_configurations = (settings.composite - .language_servers || {}) as TLanguageServerConfigurations; + const initial_configuration = (settings.composite.language_servers || + {}) as TLanguageServerConfigurations; + this.connection_manager.initial_configurations = initial_configuration; + // update the server-independent part of configuration immediately + this.connection_manager.updateConfiguration(initial_configuration); settings.changed.connect(() => { this.updateOptions(settings); @@ -223,6 +226,10 @@ export class LSPExtension implements ILSPExtension { const languageServerSettings = (options.language_servers || {}) as TLanguageServerConfigurations; + + this.connection_manager.initial_configurations = languageServerSettings; + // TODO: if priorities changed reset connections + this.connection_manager.updateConfiguration(languageServerSettings); this.connection_manager.updateServerConfigurations(languageServerSettings); } } diff --git a/packages/jupyterlab-lsp/src/manager.ts b/packages/jupyterlab-lsp/src/manager.ts index 2a4e490c9..257cc4c56 100644 --- a/packages/jupyterlab-lsp/src/manager.ts +++ b/packages/jupyterlab-lsp/src/manager.ts @@ -90,7 +90,7 @@ export class LanguageServerManager implements ILanguageServerManager { } } - return matchingSessionsKeys.sort(this._comparePriorities); + return matchingSessionsKeys.sort(this._comparePriorities.bind(this)); } get statusCode(): number { diff --git a/packages/jupyterlab-lsp/src/utils.spec.ts b/packages/jupyterlab-lsp/src/utils.spec.ts new file mode 100644 index 000000000..f0a66e380 --- /dev/null +++ b/packages/jupyterlab-lsp/src/utils.spec.ts @@ -0,0 +1,13 @@ +import { expect } from 'chai'; + +import { uris_equal } from './utils'; + +describe('uris_equal', () => { + it('should workaround Windows paths/Pyright issues', () => { + const result = uris_equal( + 'file:///d%3A/a/jupyterlab-lsp/jupyterlab-lsp/atest/output/windows_39_4/home/n%C3%B6te%20b%C3%B2%C3%B3ks/example.py', + 'file:///d:/a/jupyterlab-lsp/jupyterlab-lsp/atest/output/windows_39_4/home/n%C3%B6te%20b%C3%B2%C3%B3ks/example.py' + ); + expect(result).to.equal(true); + }); +}); diff --git a/packages/jupyterlab-lsp/src/utils.ts b/packages/jupyterlab-lsp/src/utils.ts index b61f0a463..ac9c1f262 100644 --- a/packages/jupyterlab-lsp/src/utils.ts +++ b/packages/jupyterlab-lsp/src/utils.ts @@ -2,7 +2,7 @@ import { PageConfig } from '@jupyterlab/coreutils'; import { ReadonlyJSONObject, ReadonlyJSONValue } from '@lumino/coreutils'; import mergeWith from 'lodash.mergewith'; -const RE_PATH_ANCHOR = /^file:\/\/([^\/]+|\/[A-Z]:)/; +const RE_PATH_ANCHOR = /^file:\/\/([^\/]+|\/[a-zA-Z](?::|%3A))/; export async function sleep(timeout: number) { return new Promise(resolve => { @@ -142,7 +142,11 @@ export function is_win_path(uri: string) { * lowercase the drive component of a URI */ export function normalize_win_path(uri: string) { - return uri.replace(RE_PATH_ANCHOR, it => it.toLowerCase()); + // Pyright encodes colon on Windows, see: + // https://github.com/krassowski/jupyterlab-lsp/pull/587#issuecomment-844225253 + return uri.replace(RE_PATH_ANCHOR, it => + it.replace('%3A', ':').toLowerCase() + ); } export function uri_to_contents_path(child: string, parent?: string) { diff --git a/python_packages/jupyter_lsp/jupyter_lsp/specs/__init__.py b/python_packages/jupyter_lsp/jupyter_lsp/specs/__init__.py index ecd6f4232..9081d342a 100644 --- a/python_packages/jupyter_lsp/jupyter_lsp/specs/__init__.py +++ b/python_packages/jupyter_lsp/jupyter_lsp/specs/__init__.py @@ -8,6 +8,7 @@ from .jedi_language_server import JediLanguageServer from .julia_language_server import JuliaLanguageServer from .pyls import PalantirPythonLanguageServer +from .pyright import PyrightLanguageServer from .python_lsp_server import PythonLSPServer from .r_languageserver import RLanguageServer from .sql_language_server import SQLLanguageServer @@ -28,6 +29,7 @@ md = UnifiedLanguageServer() py_palantir = PalantirPythonLanguageServer() py_lsp_server = PythonLSPServer() +pyright = PyrightLanguageServer() r = RLanguageServer() tex = Texlab() ts = JavascriptTypescriptLanguageServer() diff --git a/python_packages/jupyter_lsp/jupyter_lsp/specs/pyright.py b/python_packages/jupyter_lsp/jupyter_lsp/specs/pyright.py new file mode 100644 index 000000000..517dd8acf --- /dev/null +++ b/python_packages/jupyter_lsp/jupyter_lsp/specs/pyright.py @@ -0,0 +1,21 @@ +from .utils import NodeModuleSpec + + +class PyrightLanguageServer(NodeModuleSpec): + node_module = key = "pyright" + script = ["langserver.index.js"] + args = ["--stdio"] + languages = ["python"] + spec = dict( + display_name=key, + mime_types=["text/python", "text/x-ipython"], + urls=dict( + home="https://github.com/microsoft/pyright", + issues="https://github.com/microsoft/pyright/issues", + ), + install=dict( + npm="npm install --save-dev {}".format(key), + yarn="yarn add --dev {}".format(key), + jlpm="jlpm add --dev {}".format(key), + ), + ) diff --git a/python_packages/jupyter_lsp/setup.cfg b/python_packages/jupyter_lsp/setup.cfg index 4dddd261d..03851b196 100644 --- a/python_packages/jupyter_lsp/setup.cfg +++ b/python_packages/jupyter_lsp/setup.cfg @@ -41,6 +41,7 @@ jupyter_lsp_spec_v1 = julia-language-server = jupyter_lsp.specs:julia python-language-server = jupyter_lsp.specs:py_palantir python-lsp-server = jupyter_lsp.specs:py_lsp_server + pyright = jupyter_lsp.specs:pyright r-languageserver = jupyter_lsp.specs:r texlab = jupyter_lsp.specs:tex sql-language-server = jupyter_lsp.specs:sql diff --git a/yarn.lock b/yarn.lock index ef8cb0751..f8e9f125c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11445,6 +11445,11 @@ puppeteer@^1.17.0: rimraf "^2.6.1" ws "^6.1.0" +pyright@^1.1: + version "1.1.140" + resolved "https://registry.yarnpkg.com/pyright/-/pyright-1.1.140.tgz#2692f67b2769e664983dff3fefee4c0e4d12f4fa" + integrity sha512-isJj7cahjEK7xAy5/aLJ4TfzLJGA4SCWqPk1pLJA3k8S6VUo4FIiPrvHOd1LM2gxImqgef4rwUeHRC+vrOKLRQ== + q@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"