From b207aa8e7982c1354747e4bbaeb95dab20d2969b Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Tue, 6 Feb 2024 12:16:55 +0000 Subject: [PATCH 1/5] Sync pyright schema with v1.1.350 --- .../jupyter_lsp/specs/config/README.md | 1 + .../specs/config/pyright.schema.json | 102 ++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/python_packages/jupyter_lsp/jupyter_lsp/specs/config/README.md b/python_packages/jupyter_lsp/jupyter_lsp/specs/config/README.md index fe5dfb986..d8e1937cf 100644 --- a/python_packages/jupyter_lsp/jupyter_lsp/specs/config/README.md +++ b/python_packages/jupyter_lsp/jupyter_lsp/specs/config/README.md @@ -6,6 +6,7 @@ These are configuration schemas extracted from canonical upstreams: - [pylsp](https://github.com/python-lsp/python-lsp-server/blob/develop/pylsp/config/schema.json) - [dockerfile-language-server-nodejs](https://github.com/microsoft/vscode-docker/blob/master/package.json) - [yaml-language-server](https://github.com/redhat-developer/vscode-yaml/blob/master/package.json) +- [pyright](https://github.com/microsoft/pyright/blob/main/packages/vscode-pyright/package.json) > All of the configurations are sent to the Language Server, but only some of them > are actually acted upon, but we don't know which is which, yet. diff --git a/python_packages/jupyter_lsp/jupyter_lsp/specs/config/pyright.schema.json b/python_packages/jupyter_lsp/jupyter_lsp/specs/config/pyright.schema.json index 5e4f0214b..0ee082d2a 100644 --- a/python_packages/jupyter_lsp/jupyter_lsp/specs/config/pyright.schema.json +++ b/python_packages/jupyter_lsp/jupyter_lsp/specs/config/pyright.schema.json @@ -78,6 +78,12 @@ "default": "warning", "enum": ["none", "information", "warning", "error"] }, + "reportInvalidTypeForm": { + "type": "string", + "description": "Diagnostics for type expression that uses an invalid form.", + "default": "error", + "enum": ["none", "information", "warning", "error"] + }, "reportMissingTypeStubs": { "type": "string", "description": "Diagnostics for imports that have no corresponding type stub file (either a typeshed file or a custom type stub). The type checker requires type stubs to do its best job at analysis.", @@ -126,6 +132,72 @@ "default": "warning", "enum": ["none", "information", "warning", "error"] }, + "reportAbstractUsage": { + "type": "string", + "description": "Diagnostics for an attempt to instantiate an abstract or protocol class or use an abstract method.", + "default": "error", + "enum": ["none", "information", "warning", "error"] + }, + "reportArgumentType": { + "type": "string", + "description": "Diagnostics for a type incompatibility for an argument to a call.", + "default": "error", + "enum": ["none", "information", "warning", "error"] + }, + "reportAssertTypeFailure": { + "type": "string", + "description": "Diagnostics for a type incompatibility detected by a typing.assert_type call.", + "default": "error", + "enum": ["none", "information", "warning", "error"] + }, + "reportAssignmentType": { + "type": "string", + "description": "Diagnostics for type incompatibilities for assignments.", + "default": "error", + "enum": ["none", "information", "warning", "error"] + }, + "reportAttributeAccessIssue": { + "type": "string", + "description": "Diagnostics for issues involving attribute accesses.", + "default": "error", + "enum": ["none", "information", "warning", "error"] + }, + "reportCallIssue": { + "type": "string", + "description": "Diagnostics for issues involving call expressions and arguments.", + "default": "error", + "enum": ["none", "information", "warning", "error"] + }, + "reportInconsistentOverload": { + "type": "string", + "description": "Diagnostics for inconsistencies between function overload signatures and implementation.", + "default": "error", + "enum": ["none", "information", "warning", "error"] + }, + "reportIndexIssue": { + "type": "string", + "description": "Diagnostics related to index operations and expressions.", + "default": "error", + "enum": ["none", "information", "warning", "error"] + }, + "reportInvalidTypeArguments": { + "type": "string", + "description": "Diagnostics for invalid type argument usage.", + "default": "error", + "enum": ["none", "information", "warning", "error"] + }, + "reportNoOverloadImplementation": { + "type": "string", + "description": "Diagnostics for an overloaded function or method with a missing implementation.", + "default": "error", + "enum": ["none", "information", "warning", "error"] + }, + "reportOperatorIssue": { + "type": "string", + "description": "Diagnostics for related to unary or binary operators.", + "default": "error", + "enum": ["none", "information", "warning", "error"] + }, "reportOptionalSubscript": { "type": "string", "description": "Diagnostics for an attempt to subscript (index) a variable with an Optional type.", @@ -162,6 +234,18 @@ "default": "error", "enum": ["none", "information", "warning", "error"] }, + "reportRedeclaration": { + "type": "string", + "description": "Diagnostics for an attempt to declare the type of a symbol multiple times.", + "default": "error", + "enum": ["none", "information", "warning", "error"] + }, + "reportReturnType": { + "type": "string", + "description": "Diagnostics related to function return type compatibility.", + "default": "error", + "enum": ["none", "information", "warning", "error"] + }, "reportTypedDictNotRequiredAccess": { "type": "string", "description": "Diagnostics for an attempt to access a non-required key within a TypedDict without a check for its presence.", @@ -246,6 +330,12 @@ "default": "none", "enum": ["none", "information", "warning", "error"] }, + "reportPossiblyUnboundVariable": { + "type": "string", + "description": "Diagnostics for the use of variables that may be unbound on some code paths.", + "default": "error", + "enum": ["none", "information", "warning", "error"] + }, "reportMissingSuperCall": { "type": "string", "description": "Diagnostics for missing call to parent class for inherited `__init__` methods.", @@ -402,6 +492,12 @@ "default": "error", "enum": ["none", "information", "warning", "error"] }, + "reportUnusedExcept": { + "type": "string", + "description": "Diagnostics for unreachable except clause.", + "default": "error", + "enum": ["none", "information", "warning", "error"] + }, "reportUnusedExpression": { "type": "string", "description": "Diagnostics for simple expressions whose value is not used in any way.", @@ -468,6 +564,12 @@ "description": "Disables type completion, definitions, and references.", "scope": "resource" }, + "pyright.disableTaggedHints": { + "type": "boolean", + "default": false, + "description": "Disable hint diagnostics with special hints for grayed-out or strike-through text.", + "scope": "resource" + }, "pyright.disableOrganizeImports": { "type": "boolean", "default": false, From c93d8e80ee1bfe103181f7057f57ff366db47732 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Tue, 6 Feb 2024 13:19:43 +0000 Subject: [PATCH 2/5] Do not leak renamed settings back to settings store/editor --- packages/jupyterlab-lsp/src/index.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/jupyterlab-lsp/src/index.ts b/packages/jupyterlab-lsp/src/index.ts index 6e39265b2..2cebddcb6 100644 --- a/packages/jupyterlab-lsp/src/index.ts +++ b/packages/jupyterlab-lsp/src/index.ts @@ -9,6 +9,7 @@ export * as SCHEMA from './_schema'; /** Component- and feature-specific APIs */ export * from './api'; +import { JSONExt } from '@lumino/coreutils'; import { COMPLETION_THEME_MANAGER } from '@jupyter-lsp/completion-theme'; import { plugin as THEME_MATERIAL } from '@jupyter-lsp/theme-material'; import { plugin as THEME_VSCODE } from '@jupyter-lsp/theme-vscode'; @@ -158,13 +159,16 @@ export class LSPExtension { let languageServerSettings = (options.language_servers || {}) as TLanguageServerConfigurations; - // Add `configuration` as a copy of `serverSettings` to work with changed name upstream - // Add `rank` as a copy of priority for the same reason. + // Rename `serverSettings` to `configuration` to work with changed name upstream, + // rename `priority` to `rank` for the same reason. languageServerSettings = Object.fromEntries( Object.entries(languageServerSettings).map(([key, value]) => { - value.configuration = value.serverSettings; - value.rank = value.priority; - return [key, value]; + const copy = JSONExt.deepCopy(value); + copy.configuration = copy.serverSettings; + copy.rank = copy.priority; + delete copy.priority; + delete copy.serverSettings; + return [key, copy]; }) ); From 5117be67060dc899f981c46f6c2b7994d3feb8f7 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Tue, 6 Feb 2024 14:31:38 +0000 Subject: [PATCH 3/5] Allow nesting of settings on the final level --- packages/jupyterlab-lsp/src/settings.ts | 57 +++++++++++++++++++------ 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/packages/jupyterlab-lsp/src/settings.ts b/packages/jupyterlab-lsp/src/settings.ts index f01f043bc..67d40e44f 100644 --- a/packages/jupyterlab-lsp/src/settings.ts +++ b/packages/jupyterlab-lsp/src/settings.ts @@ -9,6 +9,7 @@ import { JSONExt, ReadonlyPartialJSONObject, ReadonlyJSONObject, + PartialJSONObject, PromiseDelegate } from '@lumino/coreutils'; import { Signal, ISignal } from '@lumino/signaling'; @@ -74,6 +75,40 @@ function getDefaults( return defaults; } +/** + * Get a mutable property matching a dotted key. + * + * Most LSP server schema properties are flattened using dotted convention, + * e.g. a key for {pylsp: {plugins: {flake8: {enabled: true}}}}` is stored + * as `pylsp.plugins.flake8.enabled`. However, some servers (e.g. pyright) + * define specific properties as only partially doted, for example + * `python.analysis.diagnosticSeverityOverrides` is an object with + * properties like `reportGeneralTypeIssues` or `reportPropertyTypeMismatch`. + * Only one level of nesting (on the finale level) is supported. + */ +function findSchemaProperty( + properties: PartialJSONObject, + key: string +): PartialJSONObject | null { + if (properties.hasOwnProperty(key)) { + return properties[key] as PartialJSONObject; + } + const parts = key.split('.'); + const prefix = parts.slice(0, -1).join('.'); + const suffix = parts[parts.length - 1]; + if (properties.hasOwnProperty(prefix)) { + const parent = properties[prefix] as PartialJSONObject; + if (parent.type !== 'object') { + return null; + } + const parentProperties = parent.properties as PartialJSONObject; + if (parentProperties.hasOwnProperty(suffix)) { + return parentProperties[suffix] as PartialJSONObject; + } + } + return null; +} + /** * Schema and user data that for validation */ @@ -315,25 +350,24 @@ export class SettingsSchemaManager { } } - // add default overrides from spec + // add default overrides from server-side spec (such as defined in `jupyter_server_config.py`) const workspaceConfigurationDefaults = serverSpec.workspace_configuration as Record | undefined; if (workspaceConfigurationDefaults) { for (const [key, value] of Object.entries( workspaceConfigurationDefaults )) { - if (!configSchema.properties.hasOwnProperty(key)) { + const property = findSchemaProperty(configSchema.properties, key); + if (!property) { this.console.warn( - '`workspace_configuration` includes an override for key not in schema', - key, - serverKey + `"workspace_configuration" includes an override for "${key}" key which was not found in ${serverKey} schema'` ); continue; } - configSchema.properties[key].default = value; + property.default = value; } } - // add server-specific default overrides from overrides.json (and pre-defined in schema) + // add server-specific default overrides from `overrides.json` (and pre-defined in schema) const serverDefaultsOverrides = defaultsOverrides && defaultsOverrides.hasOwnProperty(serverKey) ? defaultsOverrides[serverKey] @@ -342,15 +376,14 @@ export class SettingsSchemaManager { for (const [key, value] of Object.entries( serverDefaultsOverrides.serverSettings )) { - if (!configSchema.properties.hasOwnProperty(key)) { + const property = findSchemaProperty(configSchema.properties, key); + if (!property) { this.console.warn( - '`overrides.json` includes an override for key not in schema', - key, - serverKey + `"overrides.json" includes an override for "${key}" key which was not found in ${serverKey} schema` ); continue; } - configSchema.properties[key].default = value; + property.default = value as any; } } From 43e83c527f5ecb0fe03e0f57ecd82b077a47a993 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Tue, 6 Feb 2024 20:21:51 +0000 Subject: [PATCH 4/5] Fix the conflict in user settings, add tests --- packages/jupyterlab-lsp/schema/plugin.json | 1 + packages/jupyterlab-lsp/src/settings.spec.ts | 249 +++++++++++++++++++ packages/jupyterlab-lsp/src/settings.ts | 197 ++++++++++++--- 3 files changed, 406 insertions(+), 41 deletions(-) diff --git a/packages/jupyterlab-lsp/schema/plugin.json b/packages/jupyterlab-lsp/schema/plugin.json index e169eabb5..d95bcce6b 100644 --- a/packages/jupyterlab-lsp/schema/plugin.json +++ b/packages/jupyterlab-lsp/schema/plugin.json @@ -32,6 +32,7 @@ "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 dotted 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}", + "type": "object", "default": { "pyright": { "serverSettings": { diff --git a/packages/jupyterlab-lsp/src/settings.spec.ts b/packages/jupyterlab-lsp/src/settings.spec.ts index 4be25aafd..09e41ca43 100644 --- a/packages/jupyterlab-lsp/src/settings.spec.ts +++ b/packages/jupyterlab-lsp/src/settings.spec.ts @@ -1,8 +1,257 @@ import { SettingsSchemaManager } from './settings'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; +import { LanguageServerManager } from '@jupyterlab/lsp'; +import { JSONExt } from '@lumino/coreutils'; const DEAULT_SERVER_PRIORITY = 50; +const SCHEMA: ISettingRegistry.ISchema = { + type: 'object', + definitions: { + 'language-server': { + type: 'object', + default: {}, + properties: { + priority: { + title: 'Priority of the server', + type: 'number', + default: 50, + minimum: 1 + }, + serverSettings: { + title: 'Language Server Configurations', + type: 'object', + default: {}, + additionalProperties: true + } + } + } + }, + properties: { + language_servers: { + title: 'Language Server', + type: 'object', + default: { + pyright: { + serverSettings: { + 'python.analysis.useLibraryCodeForTypes': true + } + }, + 'bash-language-server': { + serverSettings: { + 'bashIde.enableSourceErrorDiagnostics': true + } + } + }, + patternProperties: { + '.*': { + $ref: '#/definitions/language-server' + } + }, + additionalProperties: { + $ref: '#/definitions/language-server' + } + } + } +}; + +const PYRIGHT_SCHEMA = { + $schema: 'http://json-schema.org/draft-07/schema#', + title: 'Pyright Language Server Configuration', + description: + 'Pyright Configuration Schema. Distributed under MIT License, Copyright (c) Microsoft Corporation.', + type: 'object', + properties: { + 'python.analysis.diagnosticSeverityOverrides': { + type: 'object', + description: + 'Allows a user to override the severity levels for individual diagnostics.', + scope: 'resource', + properties: { + reportGeneralTypeIssues: { + type: 'string', + description: + 'Diagnostics for general type inconsistencies, unsupported operations, argument/parameter mismatches, etc. Covers all of the basic type-checking rules not covered by other rules. Does not include syntax errors.', + default: 'error', + enum: ['none', 'information', 'warning', 'error'] + }, + reportPropertyTypeMismatch: { + type: 'string', + description: + 'Diagnostics for property whose setter and getter have mismatched types.', + default: 'none', + enum: ['none', 'information', 'warning', 'error'] + }, + reportFunctionMemberAccess: { + type: 'string', + description: 'Diagnostics for member accesses on functions.', + default: 'none', + enum: ['none', 'information', 'warning', 'error'] + }, + reportMissingImports: { + type: 'string', + description: + 'Diagnostics for imports that have no corresponding imported python file or type stub file.', + default: 'error', + enum: ['none', 'information', 'warning', 'error'] + }, + reportUnusedImport: { + type: 'string', + description: + 'Diagnostics for an imported symbol that is not referenced within that file.', + default: 'none', + enum: ['none', 'information', 'warning', 'error'] + }, + reportUnusedClass: { + type: 'string', + description: + 'Diagnostics for a class with a private name (starting with an underscore) that is not accessed.', + default: 'none', + enum: ['none', 'information', 'warning', 'error'] + } + } + }, + 'python.analysis.logLevel': { + type: 'string', + default: 'Information', + description: 'Specifies the level of logging for the Output panel', + enum: ['Error', 'Warning', 'Information', 'Trace'] + }, + 'python.analysis.useLibraryCodeForTypes': { + type: 'boolean', + default: false, + description: + 'Use library implementations to extract type information when type stub is not present.', + scope: 'resource' + }, + 'python.pythonPath': { + type: 'string', + default: 'python', + description: 'Path to Python, you can use a custom version of Python.', + scope: 'resource' + } + } +}; + +const COLLAPSED_PYRIGHT_SETTINGS = { + pyright: { + priority: 50, + serverSettings: { + 'python.analysis.autoImportCompletions': false, + 'python.analysis.extraPaths': [], + 'python.analysis.stubPath': 'typings', + 'python.pythonPath': 'python', + 'python.analysis.diagnosticSeverityOverrides.reportGeneralTypeIssues': + 'error', + 'python.analysis.diagnosticSeverityOverrides.reportPropertyTypeMismatch': + 'none', + 'python.analysis.diagnosticSeverityOverrides.reportFunctionMemberAccess': + 'none', + 'python.analysis.diagnosticSeverityOverrides.reportMissingImports': 'none' + } + } +}; + +function map(object: Object) { + return new Map(Object.entries(object)); +} + +const AVAILABLE_SESSIONS = map({ + pyright: null as any, + pylsp: null as any +}) as LanguageServerManager['sessions']; + describe('SettingsSchemaManager', () => { + describe('#expandDottedAsNeeded()', () => { + it('should uncollapse pyright defaults', () => { + const partiallyExpaneded = SettingsSchemaManager.expandDottedAsNeeded({ + dottedSettings: COLLAPSED_PYRIGHT_SETTINGS, + specs: map({ + pyright: { + display_name: 'pyright', + // server-specific defaults and allowed values + config_schema: PYRIGHT_SCHEMA + } + }) as LanguageServerManager['specs'] + }); + expect(partiallyExpaneded).toEqual({ + pyright: { + priority: 50, + serverSettings: { + 'python.analysis.autoImportCompletions': false, + 'python.analysis.diagnosticSeverityOverrides': { + reportFunctionMemberAccess: 'none', + reportGeneralTypeIssues: 'error', + reportMissingImports: 'none', + reportPropertyTypeMismatch: 'none' + }, + 'python.analysis.extraPaths': [], + 'python.analysis.stubPath': 'typings', + 'python.pythonPath': 'python' + } + } + }); + }); + }); + + describe('#transformSchemas()', () => { + it('should merge dotted defaults', () => { + const schema = JSONExt.deepCopy(SCHEMA) as any; + + // Set a few defaults as if these came from `overrides.json`: + // - using fully dotted name + schema.properties.language_servers.default.pyright.serverSettings[ + 'python.analysis.diagnosticSeverityOverrides.reportGeneralTypeIssues' + ] = 'warning'; + // - using nesting on final level (as defined in source pyright schema) + schema.properties.language_servers.default.pyright.serverSettings[ + 'python.analysis.diagnosticSeverityOverrides' + ] = { + reportPropertyTypeMismatch: 'warning' + }; + + const { defaults } = SettingsSchemaManager.transformSchemas({ + // plugin schema which includes overrides from `overrides.json` + schema, + specs: map({ + pyright: { + display_name: 'pyright', + // server-specific defaults and allowed values + config_schema: PYRIGHT_SCHEMA, + // overrides defined in specs files e.g. `jupyter_server_config.py` + workspace_configuration: { + // using fully dotted name + 'python.analysis.diagnosticSeverityOverrides.reportFunctionMemberAccess': + 'warning', + // using nesting on final level (as defined in source pyright schema) + 'python.analysis.diagnosticSeverityOverrides': { + reportUnusedImport: 'warning' + } + } + } + }) as LanguageServerManager['specs'], + sessions: AVAILABLE_SESSIONS + }); + const defaultOverrides = + defaults.pyright.serverSettings[ + 'python.analysis.diagnosticSeverityOverrides' + ]; + expect(defaultOverrides).toEqual({ + // `overrides.json`: + // - should provide `reportGeneralTypeIssues` defined with fully dotted key + reportGeneralTypeIssues: 'warning', + // - should provide `reportPropertyTypeMismatch` defined with nesting on final level + // `jupyter_server_config.py`: + reportPropertyTypeMismatch: 'warning', + // - should provide `reportFunctionMemberAccess` defined with fully dotted key + reportFunctionMemberAccess: 'warning', + // - should provide `reportUnusedImport` defined with nesting on final level + reportUnusedImport: 'warning' + // should NOT include `reportUnusedClass` default defined in server schema + }); + }); + }); + describe('#mergeByServer()', () => { it('prioritises user `priority` over the default', () => { const defaults = { diff --git a/packages/jupyterlab-lsp/src/settings.ts b/packages/jupyterlab-lsp/src/settings.ts index 67d40e44f..4d420981c 100644 --- a/packages/jupyterlab-lsp/src/settings.ts +++ b/packages/jupyterlab-lsp/src/settings.ts @@ -4,7 +4,7 @@ import { ISettingRegistry, ISchemaValidator } from '@jupyterlab/settingregistry'; -import { TranslationBundle } from '@jupyterlab/translation'; +import { TranslationBundle, nullTranslator } from '@jupyterlab/translation'; import { JSONExt, ReadonlyPartialJSONObject, @@ -21,7 +21,7 @@ import { renderCollapseConflicts } from './components/serverSettings'; import { ILSPLogConsole } from './tokens'; -import { collapseToDotted } from './utils'; +import { collapseToDotted, expandDottedPaths } from './utils'; type ValueOf = T[keyof T]; type ServerSchemaWrapper = ValueOf< @@ -76,7 +76,7 @@ function getDefaults( } /** - * Get a mutable property matching a dotted key. + * Get a mutable property matching a dotted key and a properly nested value. * * Most LSP server schema properties are flattened using dotted convention, * e.g. a key for {pylsp: {plugins: {flake8: {enabled: true}}}}` is stored @@ -86,12 +86,13 @@ function getDefaults( * properties like `reportGeneralTypeIssues` or `reportPropertyTypeMismatch`. * Only one level of nesting (on the finale level) is supported. */ -function findSchemaProperty( +function nestInSchema( properties: PartialJSONObject, - key: string -): PartialJSONObject | null { + key: string, + value: PartialJSONObject +): { property: PartialJSONObject; value: PartialJSONObject } | null { if (properties.hasOwnProperty(key)) { - return properties[key] as PartialJSONObject; + return { property: properties[key] as PartialJSONObject, value }; } const parts = key.split('.'); const prefix = parts.slice(0, -1).join('.'); @@ -103,12 +104,33 @@ function findSchemaProperty( } const parentProperties = parent.properties as PartialJSONObject; if (parentProperties.hasOwnProperty(suffix)) { - return parentProperties[suffix] as PartialJSONObject; + return { + property: parent, + value: { [suffix]: value } + }; } } return null; } +function mergePropertyDefault( + property: PartialJSONObject, + value: PartialJSONObject +) { + if ( + property.type === 'object' && + typeof property.default === 'object' && + typeof value === 'object' + ) { + property.default = { + ...property.default, + ...value + }; + } else { + property.default = value; + } +} + /** * Schema and user data that for validation */ @@ -267,6 +289,36 @@ export class SettingsSchemaManager { schema: ISettingRegistry.ISchema ) { const languageServerManager = this.options.languageServerManager; + + const { properties, defaults } = SettingsSchemaManager.transformSchemas({ + schema, + // TODO: expose `specs` upstream and use `ILanguageServerManager` instead + specs: (languageServerManager as LanguageServerManager).specs, + sessions: languageServerManager.sessions, + console: this.console, + trans: this.options.trans + }); + + schema.properties!.language_servers.properties = properties; + schema.properties!.language_servers.default = defaults; + + this._validateSchemaLater(plugin, schema).catch(this.console.warn); + this._defaults = defaults; + } + + /** + * Transform the plugin schema defaults, properties and descriptions + */ + static transformSchemas(options: { + schema: ISettingRegistry.ISchema; + specs: LanguageServerManager['specs']; + sessions: ILanguageServerManager['sessions']; + console?: ILSPLogConsole; + trans?: TranslationBundle; + }) { + const { schema, sessions, specs } = options; + const trans = options.trans ?? nullTranslator.load('jupyterlab-lsp'); + const console = options.console ?? window.console; const baseServerSchema = (schema.definitions as any)['language-server'] as { description: string; title: string; @@ -285,47 +337,41 @@ export class SettingsSchemaManager { | Record | undefined; - // TODO: expose `specs` upstream - for (let [serverKey, serverSpec] of ( - languageServerManager as LanguageServerManager - ).specs.entries()) { + for (let [serverKey, serverSpec] of specs.entries()) { if ((serverKey as string) === '') { - this.console.warn( - 'Empty server key - skipping transformation for', - serverSpec + console.warn( + `Empty server key - skipping transformation for ${serverSpec}` ); continue; } const configSchema = serverSpec.config_schema; if (!configSchema) { - this.console.warn( - 'No config schema - skipping transformation for', - serverKey + console.warn( + `No config schema - skipping transformation for ${serverKey}` ); continue; } if (!configSchema.properties) { - this.console.warn( - 'No properties in config schema - skipping transformation for', - serverKey + console.warn( + `No properties in config schema - skipping transformation for ${serverKey}` ); continue; } // let user know if server not available (installed, etc) - if (!languageServerManager.sessions.has(serverKey)) { - configSchema.description = this.options.trans.__( + if (!sessions.has(serverKey)) { + configSchema.description = trans.__( 'Settings that would be passed to `%1` server (this server was not detected as installed during startup) in `workspace/didChangeConfiguration` notification.', serverSpec.display_name ); } else { - configSchema.description = this.options.trans.__( + configSchema.description = trans.__( 'Settings to be passed to %1 in `workspace/didChangeConfiguration` notification.', serverSpec.display_name ); } - configSchema.title = this.options.trans.__('Workspace Configuration'); + configSchema.title = trans.__('Workspace Configuration'); // resolve refs for (let [key, value] of Object.entries(configSchema.properties)) { @@ -339,14 +385,14 @@ export class SettingsSchemaManager { const definitionID = value['$ref'].substring(14); const definition = configSchema.definitions[definitionID]; if (definition == null) { - this.console.warn('Definition not found'); + console.warn('Definition not found'); } for (let [defKey, defValue] of Object.entries(definition)) { configSchema.properties[key][defKey] = defValue; } delete value.$ref; } else { - this.console.warn('Unsupported $ref', value['$ref']); + console.warn('Unsupported $ref', value['$ref']); } } @@ -357,14 +403,14 @@ export class SettingsSchemaManager { for (const [key, value] of Object.entries( workspaceConfigurationDefaults )) { - const property = findSchemaProperty(configSchema.properties, key); - if (!property) { - this.console.warn( + const nested = nestInSchema(configSchema.properties, key, value); + if (!nested) { + console.warn( `"workspace_configuration" includes an override for "${key}" key which was not found in ${serverKey} schema'` ); continue; } - property.default = value; + mergePropertyDefault(nested.property, nested.value); } } // add server-specific default overrides from `overrides.json` (and pre-defined in schema) @@ -376,14 +422,18 @@ export class SettingsSchemaManager { for (const [key, value] of Object.entries( serverDefaultsOverrides.serverSettings )) { - const property = findSchemaProperty(configSchema.properties, key); - if (!property) { - this.console.warn( + const nested = nestInSchema( + configSchema.properties, + key, + value as any + ); + if (!nested) { + console.warn( `"overrides.json" includes an override for "${key}" key which was not found in ${serverKey} schema` ); continue; } - property.default = value as any; + mergePropertyDefault(nested.property, nested.value); } } @@ -399,11 +449,67 @@ export class SettingsSchemaManager { }; } - schema.properties!.language_servers.properties = knownServersConfig; - schema.properties!.language_servers.default = defaults; + return { + properties: knownServersConfig, + defaults + }; + } - this._validateSchemaLater(plugin, schema).catch(this.console.warn); - this._defaults = defaults; + /** + * Expands dotted values into nested properties when the server config schema + * indicates that this is needed. The schema is passed within the specs. + * + * This is needed because some settings, specifically pright's + * `python.analysis.diagnosticSeverityOverrides` are defined as nested. + */ + static expandDottedAsNeeded(options: { + dottedSettings: LanguageServerSettings; + specs: LanguageServerManager['specs']; + }): LanguageServerSettings { + const specs = options.specs; + const partiallyUncollapsed = JSONExt.deepCopy(options.dottedSettings); + + for (let [serverKey, serverSpec] of specs.entries()) { + const configSchema = serverSpec.config_schema; + if (!partiallyUncollapsed.hasOwnProperty(serverKey)) { + continue; + } + const settings = partiallyUncollapsed[serverKey].serverSettings; + if (!configSchema || !settings) { + continue; + } + const expanded = expandDottedPaths(settings); + + for (const [path, property] of Object.entries( + configSchema.properties + )) { + if (property.type === 'object') { + let value = expanded; + for (const part of path.split('.')) { + value = value[part] as ReadonlyJSONObject; + if (typeof value === 'undefined') { + break; + } + } + if (typeof value === 'undefined') { + continue; + } + // Add the uncollapsed value + settings[path] = value; + // Remove the collapsed values + for (const k of Object.keys(value)) { + const key = path + '.' + k; + if (!settings.hasOwnProperty(key)) { + throw Error( + 'Internal inconsistency: collapsed settings state does not match expanded object' + ); + } + delete settings[key]; + } + } + } + } + return partiallyUncollapsed; } /** @@ -434,11 +540,20 @@ export class SettingsSchemaManager { composite.language_servers ); - composite.language_servers = SettingsSchemaManager.mergeByServer( + const merged = SettingsSchemaManager.mergeByServer( collapsedDefaults.settings, collapsedUser.settings ); - this._lastUserServerSettingsDoted = composite.language_servers; + + // Uncollapse settings which need to be in the expanded form + const languageServerManager = this.options.languageServerManager; + const uncollapsed = SettingsSchemaManager.expandDottedAsNeeded({ + dottedSettings: merged, + specs: (languageServerManager as LanguageServerManager).specs + }); + + composite.language_servers = uncollapsed; + this._lastUserServerSettingsDoted = uncollapsed; if (Object.keys(collapsedUser.conflicts).length > 0) { this._warnConflicts( From db0673000e676ce72c4e4a690007e58d9f0cf3c5 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Wed, 7 Feb 2024 12:19:08 +0000 Subject: [PATCH 5/5] Lint --- packages/.eslintrc.js | 4 +++- packages/jupyterlab-lsp/src/index.ts | 2 +- packages/jupyterlab-lsp/src/settings.spec.ts | 7 ++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/.eslintrc.js b/packages/.eslintrc.js index 1ad23435d..68c33130e 100644 --- a/packages/.eslintrc.js +++ b/packages/.eslintrc.js @@ -40,6 +40,7 @@ module.exports = { ignoreGlobals: true, allow: [ 'cell_type', + 'config_schema', 'execution_count', 'language_info', 'nbconvert_exporter', @@ -52,7 +53,8 @@ module.exports = { 'lsp_to_ce', 'ce_to_cm', 'cm_to_lsp', - 'lsp_to_cm' + 'lsp_to_cm', + 'workspace_configuration' ] } ], diff --git a/packages/jupyterlab-lsp/src/index.ts b/packages/jupyterlab-lsp/src/index.ts index 2cebddcb6..c736aa5d8 100644 --- a/packages/jupyterlab-lsp/src/index.ts +++ b/packages/jupyterlab-lsp/src/index.ts @@ -9,7 +9,6 @@ export * as SCHEMA from './_schema'; /** Component- and feature-specific APIs */ export * from './api'; -import { JSONExt } from '@lumino/coreutils'; import { COMPLETION_THEME_MANAGER } from '@jupyter-lsp/completion-theme'; import { plugin as THEME_MATERIAL } from '@jupyter-lsp/theme-material'; import { plugin as THEME_VSCODE } from '@jupyter-lsp/theme-vscode'; @@ -27,6 +26,7 @@ import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { IStatusBar } from '@jupyterlab/statusbar'; import { ITranslator, nullTranslator } from '@jupyterlab/translation'; import { IFormRendererRegistry } from '@jupyterlab/ui-components'; +import { JSONExt } from '@lumino/coreutils'; import '../style/index.css'; diff --git a/packages/jupyterlab-lsp/src/settings.spec.ts b/packages/jupyterlab-lsp/src/settings.spec.ts index 09e41ca43..f4fc37912 100644 --- a/packages/jupyterlab-lsp/src/settings.spec.ts +++ b/packages/jupyterlab-lsp/src/settings.spec.ts @@ -1,8 +1,9 @@ -import { SettingsSchemaManager } from './settings'; -import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { LanguageServerManager } from '@jupyterlab/lsp'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { JSONExt } from '@lumino/coreutils'; +import { SettingsSchemaManager } from './settings'; + const DEAULT_SERVER_PRIORITY = 50; const SCHEMA: ISettingRegistry.ISchema = { @@ -152,7 +153,7 @@ const COLLAPSED_PYRIGHT_SETTINGS = { } }; -function map(object: Object) { +function map(object: Record) { return new Map(Object.entries(object)); }