Skip to content

Commit 57055c1

Browse files
committed
Add priority setting for servers handling the same language
1 parent e85d73f commit 57055c1

File tree

12 files changed

+159
-76
lines changed

12 files changed

+159
-76
lines changed

atest/03_Notebook.robot

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Moving Cells Around
4747
Foreign Extractors
4848
${file} = Set Variable Foreign extractors.ipynb
4949
Configure JupyterLab Plugin
50-
... {"language_servers": {"texlab": {"serverSettings": {"latex.lint.onChange": true}}, "bash-langauge-server": {"bashIde.highlightParsingErrors": true}}}
50+
... {"language_servers": {"texlab": {"serverSettings": {"latex.lint.onChange": true}}, "bash-langauge-server": {"bashIde.highlightParsingErrors": true}}, "pylsp": {"priority": 1000}}
5151
Capture Page Screenshot 10-configured.png
5252
Reset Application State
5353
Setup Notebook Python ${file}

docs/Configuring.ipynb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@
6666
" `mkdir \"new directory\"` should be split into `[\"mkdir\", \"new directory\"]`;\n",
6767
" If you have Python installed, you can use `shlex.split(\"your command\")` to\n",
6868
" get such an array.\n",
69-
"- the `languages` which the language server will respond to, and\n",
69+
"- the `languages` which the language server will respond to (language names\n",
70+
" should be given in lowercase), and\n",
7071
"- the schema `version` of the spec (currently `2`)\n",
7172
"- `mime_types` by which the notebooks and files will be matched to the language\n",
7273
" server:\n",

docs/Language Servers.ipynb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,9 @@
137137
"\n",
138138
"These servers have support for notebooks and file editors. The `pyls` and\n",
139139
"`r-languageserver` are well-tested, while `jedi` and `Julia` servers are\n",
140-
"experimental. Please only install one language server per language (`jedi` or\n",
141-
"`pyls` for Python - not both)."
140+
"experimental. If you choose to install multiple language servers for the same\n",
141+
"language, the one with the highest `priority` (which can be set in the _Advanced\n",
142+
"Settings Editor_) will be used."
142143
]
143144
},
144145
{

packages/jupyterlab-lsp/schema/plugin.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
"description": "Configuration to be sent to language server over LSP when initialized: see the specific language server's documentation for more",
1717
"type": "object",
1818
"default": {}
19+
},
20+
"priority": {
21+
"title": "Priority of the server",
22+
"description": "When multiple servers match specific document/language, the server with higher priority will be used",
23+
"type": "number",
24+
"default": 50
1925
}
2026
}
2127
}

packages/jupyterlab-lsp/src/components/statusbar.tsx

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,12 @@ import { LSPConnection } from '../connection';
3535
import { DocumentConnectionManager } from '../connection_manager';
3636
import { SERVER_EXTENSION_404 } from '../errors';
3737
import { LanguageServerManager } from '../manager';
38-
import { ILSPAdapterManager, ILanguageServerManager } from '../tokens';
38+
import {
39+
ILSPAdapterManager,
40+
ILanguageServerManager,
41+
TSessionMap,
42+
TLanguageServerId
43+
} from '../tokens';
3944
import { VirtualDocument, collect_documents } from '../virtual/document';
4045

4146
import { codeCheckIcon, codeClockIcon, codeWarningIcon } from './icons';
@@ -464,34 +469,44 @@ export namespace LSPStatus {
464469
}, this);
465470
}
466471

467-
get available_servers(): Array<SCHEMA.LanguageServerSession> {
468-
return Array.from(this.language_server_manager.sessions.values());
472+
get available_servers(): TSessionMap {
473+
return this.language_server_manager.sessions;
469474
}
470475

471476
get supported_languages(): Set<string> {
472477
const languages = new Set<string>();
473-
for (let server of this.available_servers) {
478+
for (let server of this.available_servers.values()) {
474479
for (let language of server.spec.languages) {
475480
languages.add(language.toLocaleLowerCase());
476481
}
477482
}
478483
return languages;
479484
}
480485

481-
private is_server_running(server: SCHEMA.LanguageServerSession): boolean {
482-
for (let language of server.spec.languages) {
483-
if (this.detected_languages.has(language.toLocaleLowerCase())) {
486+
private is_server_running(
487+
id: TLanguageServerId,
488+
server: SCHEMA.LanguageServerSession
489+
): boolean {
490+
for (const language of this.detected_languages) {
491+
const matchedServers = this.language_server_manager.getMatchingServers({
492+
language
493+
});
494+
// TODO server.status === "started" ?
495+
// TODO update once multiple servers are allowed
496+
if (matchedServers[0] === id) {
484497
return true;
485498
}
486499
}
487-
return false;
488500
}
489501

490502
get documents_by_server(): Map<
491503
SCHEMA.LanguageServerSession,
492504
Map<string, VirtualDocument[]>
493505
> {
494-
let data = new Map();
506+
let data = new Map<
507+
SCHEMA.LanguageServerSession,
508+
Map<string, VirtualDocument[]>
509+
>();
495510
if (!this.adapter?.virtual_editor) {
496511
return data;
497512
}
@@ -501,19 +516,17 @@ export namespace LSPStatus {
501516

502517
for (let document of documents.values()) {
503518
let language = document.language.toLocaleLowerCase();
504-
let servers = this.available_servers.filter(
505-
server => server.spec.languages.indexOf(language) !== -1
519+
let server_ids = this._connection_manager.language_server_manager.getMatchingServers(
520+
{ language: document.language }
506521
);
507-
if (servers.length > 1) {
508-
console.warn('More than one server per language for', language);
509-
}
510-
if (servers.length === 0) {
522+
if (server_ids.length === 0) {
511523
continue;
512524
}
513-
let server = servers[0];
525+
// For now only use the server with the highest priority
526+
let server = this.language_server_manager.sessions.get(server_ids[0]);
514527

515528
if (!data.has(server)) {
516-
data.set(server, new Map<string, VirtualDocument>());
529+
data.set(server, new Map<string, VirtualDocument[]>());
517530
}
518531

519532
let documents_map = data.get(server);
@@ -529,9 +542,9 @@ export namespace LSPStatus {
529542
}
530543

531544
get servers_available_not_in_use(): Array<SCHEMA.LanguageServerSession> {
532-
return this.available_servers.filter(
533-
server => !this.is_server_running(server)
534-
);
545+
return [...this.available_servers.entries()]
546+
.filter(([id, server]) => !this.is_server_running(id, server))
547+
.map(([id, server]) => server);
535548
}
536549

537550
get detected_languages(): Set<string> {
@@ -577,7 +590,7 @@ export namespace LSPStatus {
577590

578591
detected_documents.forEach((document, uri) => {
579592
let connection = this._connection_manager.connections.get(uri);
580-
let server_id = this._connection_manager.language_server_manager.getServerId(
593+
let server_id = this._connection_manager.language_server_manager.getMatchingServers(
581594
{ language: document.language }
582595
);
583596
if (server_id !== null) {

packages/jupyterlab-lsp/src/connection_manager.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
22
import { Signal } from '@lumino/signaling';
3+
import type * as protocol from 'vscode-languageserver-protocol';
34

45
import type * as ConnectionModuleType from './connection';
56
import {
67
ILSPLogConsole,
7-
ILanguageServerConfiguration,
88
ILanguageServerManager,
99
TLanguageServerConfigurations,
10-
TLanguageServerId
10+
TLanguageServerId,
11+
TServerKeys
1112
} from './tokens';
1213
import { expandDottedPaths, sleep, until_ready } from './utils';
1314
import { IForeignContext, VirtualDocument } from './virtual/document';
@@ -134,9 +135,14 @@ export class DocumentConnectionManager {
134135
language
135136
);
136137

137-
const language_server_id = this.language_server_manager.getServerId({
138+
const matchingServers = this.language_server_manager.getMatchingServers({
138139
language
139140
});
141+
this.console.debug('Matching servers: ', matchingServers);
142+
143+
// for now use only the server with the highest priority.
144+
const language_server_id =
145+
matchingServers.length === 0 ? null : matchingServers[0];
140146

141147
// lazily load 1) the underlying library (1.5mb) and/or 2) a live WebSocket-
142148
// like connection: either already connected or potentially in the process
@@ -161,13 +167,21 @@ export class DocumentConnectionManager {
161167
* "serverSettings" keyword in the setting registry. New keywords can
162168
* be added and extra functionality implemented here when needed.
163169
*/
164-
public updateServerConfigurations(allServerSettings: any) {
165-
for (let language_server_id in allServerSettings) {
166-
const parsedSettings = expandDottedPaths(
167-
allServerSettings[language_server_id].serverSettings
168-
);
170+
public updateServerConfigurations(
171+
allServerSettings: TLanguageServerConfigurations
172+
) {
173+
let language_server_id: TServerKeys;
174+
this.language_server_manager.setConfiguration(allServerSettings);
175+
176+
for (language_server_id in allServerSettings) {
177+
if (!allServerSettings.hasOwnProperty(language_server_id)) {
178+
continue;
179+
}
180+
const rawSettings = allServerSettings[language_server_id];
169181

170-
const serverSettings: ILanguageServerConfiguration = {
182+
const parsedSettings = expandDottedPaths(rawSettings.serverSettings);
183+
184+
const serverSettings: protocol.DidChangeConfigurationParams = {
171185
settings: parsedSettings
172186
};
173187

@@ -351,9 +365,14 @@ export namespace DocumentConnectionManager {
351365
? rootUri
352366
: virtualDocumentsUri;
353367

354-
const language_server_id = Private.getLanguageServerManager().getServerId({
355-
language
356-
});
368+
// for now take the best match only
369+
const matchingServers = Private.getLanguageServerManager().getMatchingServers(
370+
{
371+
language
372+
}
373+
);
374+
const language_server_id =
375+
matchingServers.length === 0 ? null : matchingServers[0];
357376

358377
if (language_server_id === null) {
359378
throw `No language server installed for language ${language}`;
@@ -441,7 +460,7 @@ namespace Private {
441460

442461
export function updateServerConfiguration(
443462
language_server_id: TLanguageServerId,
444-
settings: ILanguageServerConfiguration
463+
settings: protocol.DidChangeConfigurationParams
445464
): void {
446465
const connection = _connections.get(language_server_id);
447466
if (connection) {

packages/jupyterlab-lsp/src/editor_integration/testutils.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export interface ITestEnvironment {
7171
export class MockLanguageServerManager extends LanguageServerManager {
7272
async fetchSessions() {
7373
this._sessions = new Map();
74-
this._sessions.set('pyls', {
74+
this._sessions.set('pylsp', {
7575
spec: {
7676
languages: ['python']
7777
}
@@ -111,7 +111,9 @@ export class MockExtension implements ILSPExtension {
111111
this.app = null;
112112
this.feature_manager = new FeatureManager();
113113
this.editor_type_manager = new VirtualEditorManager();
114-
this.language_server_manager = new MockLanguageServerManager({});
114+
this.language_server_manager = new MockLanguageServerManager({
115+
console: new BrowserConsole()
116+
});
115117
this.connection_manager = new DocumentConnectionManager({
116118
language_server_manager: this.language_server_manager,
117119
console: new BrowserConsole()

packages/jupyterlab-lsp/src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,12 @@ export class LSPExtension implements ILSPExtension {
146146
status_bar: IStatusBar | null
147147
) {
148148
const trans = (translator || nullTranslator).load('jupyterlab-lsp');
149-
this.language_server_manager = new LanguageServerManager({});
149+
this.language_server_manager = new LanguageServerManager({
150+
console: this.console.scope('LanguageServerManager')
151+
});
150152
this.connection_manager = new DocumentConnectionManager({
151153
language_server_manager: this.language_server_manager,
152-
console: this.console
154+
console: this.console.scope('DocumentConnectionManager')
153155
});
154156

155157
const statusButtonExtension = new StatusButtonExtension({

packages/jupyterlab-lsp/src/manager.ts

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import { ServerConnection } from '@jupyterlab/services';
33
import { Signal } from '@lumino/signaling';
44

55
import * as SCHEMA from './_schema';
6-
import { ILanguageServerManager, TSessionMap } from './tokens';
6+
import {
7+
ILanguageServerManager,
8+
ILSPLogConsole,
9+
TLanguageServerConfigurations,
10+
TLanguageServerId,
11+
TSessionMap
12+
} from './tokens';
713

814
export class LanguageServerManager implements ILanguageServerManager {
915
protected _sessionsChanged: Signal<ILanguageServerManager, void> = new Signal<
@@ -16,13 +22,17 @@ export class LanguageServerManager implements ILanguageServerManager {
1622
private _statusCode: number;
1723
private _retries: number;
1824
private _retriesInterval: number;
25+
private _configuration: TLanguageServerConfigurations;
26+
private console: ILSPLogConsole;
1927

2028
constructor(options: ILanguageServerManager.IOptions) {
2129
this._settings = options.settings || ServerConnection.makeSettings();
2230
this._baseUrl = options.baseUrl || PageConfig.getBaseUrl();
2331
this._retries = options.retries || 2;
2432
this._retriesInterval = options.retriesInterval || 10000;
2533
this._statusCode = null;
34+
this._configuration = {};
35+
this.console = options.console;
2636
this.fetchSessions().catch(console.warn);
2737
}
2838

@@ -38,16 +48,44 @@ export class LanguageServerManager implements ILanguageServerManager {
3848
return this._sessions;
3949
}
4050

41-
getServerId(options: ILanguageServerManager.IGetServerIdOptions) {
51+
setConfiguration(configuration: TLanguageServerConfigurations): void {
52+
this._configuration = configuration;
53+
}
54+
55+
getMatchingServers(options: ILanguageServerManager.IGetServerIdOptions) {
56+
const matchingSessionsKeys: TLanguageServerId[] = [];
57+
const config = this._configuration;
58+
4259
// most things speak language
60+
// if language is not known, it is guessed based on MIME type earlier
61+
// so some language should be available by now (which can be not obvious, e.g. "plain" for txt documents)
4362
for (const [key, session] of this._sessions.entries()) {
4463
if (options.language) {
45-
if (session.spec.languages.indexOf(options.language) !== -1) {
46-
return key;
64+
if (
65+
session.spec.languages.indexOf(
66+
options.language.toLocaleLowerCase()
67+
) !== -1
68+
) {
69+
matchingSessionsKeys.push(key);
4770
}
71+
} else {
72+
this.console.error(
73+
'Cannot match server by language: language not available; ensure that kernel and specs provide language and MIME type'
74+
);
4875
}
4976
}
50-
return null;
77+
78+
return matchingSessionsKeys.sort((a, b) => {
79+
const a_priority = config[a]?.priority ?? 50;
80+
const b_priority = config[b]?.priority ?? 50;
81+
if (a_priority == b_priority) {
82+
this.console.warn(
83+
`Two matching servers: ${a} and ${b} have the same priority; choose which one to use by changing the priority in Advanced Settings Editor`
84+
);
85+
}
86+
// higher priority = higher in the list (descending order)
87+
return b_priority - a_priority;
88+
});
5189
}
5290

5391
get statusCode(): number {
@@ -81,19 +119,21 @@ export class LanguageServerManager implements ILanguageServerManager {
81119
return;
82120
}
83121

84-
for (const key of Object.keys(sessions)) {
85-
if (this._sessions.has(key)) {
86-
Object.assign(this._sessions.get(key), sessions[key]);
122+
for (let key of Object.keys(sessions)) {
123+
let id: TLanguageServerId = key as TLanguageServerId;
124+
if (this._sessions.has(id)) {
125+
Object.assign(this._sessions.get(id), sessions[key]);
87126
} else {
88-
this._sessions.set(key, sessions[key]);
127+
this._sessions.set(id, sessions[key]);
89128
}
90129
}
91130

92131
const oldKeys = this._sessions.keys();
93132

94133
for (const oldKey in oldKeys) {
95134
if (!sessions[oldKey]) {
96-
this._sessions.delete(oldKey);
135+
let oldId = oldKey as TLanguageServerId;
136+
this._sessions.delete(oldId);
97137
}
98138
}
99139

0 commit comments

Comments
 (0)