Skip to content

Commit b4d2707

Browse files
committed
Implement display of detail, source, and extra settings
1 parent 913bfd0 commit b4d2707

File tree

4 files changed

+179
-43
lines changed

4 files changed

+179
-43
lines changed

packages/jupyterlab-lsp/src/features/completion/completion.ts

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { IDocumentConnectionData } from '../../connection_manager';
1919
import { ILSPAdapterManager, ILSPLogConsole } from '../../tokens';
2020
import { NotebookAdapter } from '../../adapters/notebook/notebook';
2121
import { ILSPCompletionThemeManager } from '@krassowski/completion-theme/lib/types';
22-
import { LSPCompletionRenderer } from './renderer';
22+
import { ICompletionData, LSPCompletionRenderer } from './renderer';
2323
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
2424
import { LSPCompleterModel } from './model';
2525
import { LazyCompletionItem } from './item';
@@ -105,21 +105,44 @@ export class CompletionLabIntegration implements IFeatureLabIntegration {
105105
console: console.scope('renderer')
106106
});
107107
this.renderer.activeChanged.connect(this.active_completion_changed, this);
108+
this.renderer.itemShown.connect(this.resolve_and_update, this);
108109
adapterManager.adapterChanged.connect(this.swap_adapter, this);
109110
settings.changed.connect(() => {
110111
completionThemeManager.set_theme(this.settings.composite.theme);
111112
completionThemeManager.set_icons_overrides(
112113
this.settings.composite.typesMap
113114
);
114-
this.model.settings.caseSensitive = this.settings.composite.caseSensitive;
115-
this.model.settings.includePerfectMatches = this.settings.composite.includePerfectMatches;
115+
if (this.current_completion_handler) {
116+
this.model.settings.caseSensitive = this.settings.composite.caseSensitive;
117+
this.model.settings.includePerfectMatches = this.settings.composite.includePerfectMatches;
118+
}
116119
});
117120
}
118121

122+
protected fetchDocumentation(item: LazyCompletionItem): void {
123+
if (!item) {
124+
return;
125+
}
126+
item
127+
.resolve()
128+
.then(resolvedCompletionItem => {
129+
this.set_doc_panel_placeholder(false);
130+
if (resolvedCompletionItem === null) {
131+
return;
132+
}
133+
this.refresh_doc_panel(item);
134+
})
135+
.catch(e => {
136+
this.set_doc_panel_placeholder(false);
137+
console.warn(e);
138+
});
139+
}
140+
119141
active_completion_changed(
120142
renderer: LSPCompletionRenderer,
121-
item: LazyCompletionItem
143+
active_completion: ICompletionData
122144
) {
145+
let { item } = active_completion;
123146
if (!item.supportsResolution()) {
124147
if (item.isDocumentationMarkdown) {
125148
// TODO: remove once https://github.com/jupyterlab/jupyterlab/pull/9663 is merged and released
@@ -130,7 +153,7 @@ export class CompletionLabIntegration implements IFeatureLabIntegration {
130153

131154
if (item.needsResolution()) {
132155
this.set_doc_panel_placeholder(true);
133-
item.fetchDocumentation();
156+
this.fetchDocumentation(item);
134157
} else if (item.isResolved()) {
135158
this.refresh_doc_panel(item);
136159
}
@@ -141,11 +164,46 @@ export class CompletionLabIntegration implements IFeatureLabIntegration {
141164

142165
if (index - 1 >= 0) {
143166
const previous = items[index - 1] as LazyCompletionItem;
144-
previous?.self?.fetchDocumentation();
167+
this.resolve_and_update_from_item(previous?.self);
145168
}
146169
if (index + 1 < items.length) {
147170
const next = items[index + 1] as LazyCompletionItem;
148-
next?.self?.fetchDocumentation();
171+
this.resolve_and_update_from_item(next?.self);
172+
}
173+
}
174+
175+
private resolve_and_update_from_item(item: LazyCompletionItem) {
176+
if (!item) {
177+
return;
178+
}
179+
this.resolve_and_update(this.renderer, {
180+
item: item,
181+
element: item.element
182+
});
183+
}
184+
185+
private resolve_and_update(
186+
renderer: LSPCompletionRenderer,
187+
active_completion: ICompletionData
188+
) {
189+
let { item, element } = active_completion;
190+
if (!item.supportsResolution()) {
191+
this.renderer.updateExtraInfo(item, element);
192+
return;
193+
}
194+
195+
if (item.isResolved()) {
196+
this.renderer.updateExtraInfo(item, element);
197+
} else {
198+
// supportsResolution as otherwise would short-circuit above
199+
item
200+
.resolve()
201+
.then(resolvedCompletionItem => {
202+
this.renderer.updateExtraInfo(item, element);
203+
})
204+
.catch(e => {
205+
this.console.warn(e);
206+
});
149207
}
150208
}
151209

packages/jupyterlab-lsp/src/features/completion/completion_handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ export class LSPConnector
429429
return {
430430
label: item.text as string,
431431
insertText: item.text as string,
432-
type: item.type as string,
432+
type: item.type === '<unknown>' ? undefined : (item.type as string),
433433
icon: this.icon_for(item.type as string),
434434
sortText: this.kernel_completions_first ? 'a' : 'z'
435435
};

packages/jupyterlab-lsp/src/features/completion/item.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { CompletionHandler } from '@jupyterlab/completer';
22
import { LabIcon } from '@jupyterlab/ui-components';
33
import * as lsProtocol from 'vscode-languageserver-types';
44
import { LSPConnector } from './completion_handler';
5+
import { until_ready } from '../../utils';
56

67
/**
78
* To be upstreamed
@@ -43,6 +44,7 @@ export class LazyCompletionItem implements IExtendedCompletionItem {
4344
* performed by the JupyterLab completer internals.
4445
*/
4546
public self: LazyCompletionItem;
47+
public element: HTMLLIElement;
4648
private _currentInsertText: string;
4749

4850
get isDocumentationMarkdown(): boolean {
@@ -137,32 +139,35 @@ export class LazyCompletionItem implements IExtendedCompletionItem {
137139
return this._resolved;
138140
}
139141

140-
public fetchDocumentation(): void {
141-
if (!this.needsResolution()) {
142-
return;
142+
/**
143+
* Resolve (fetch) details such as documentation.
144+
*/
145+
public resolve(): Promise<lsProtocol.CompletionItem> {
146+
if (this._resolved) {
147+
return Promise.resolve(this);
148+
}
149+
if (!this.supportsResolution()) {
150+
return Promise.resolve(this);
151+
}
152+
if (this._requested_resolution) {
153+
return until_ready(() => this._resolved, 100, 50).then(() => this);
143154
}
144155

145156
const connection = this.connector.get_connection(this.uri);
146157

147158
this._requested_resolution = true;
148159

149-
connection
160+
return connection
150161
.getCompletionResolve(this.match)
151162
.then(resolvedCompletionItem => {
152-
this.connector.lab_integration.set_doc_panel_placeholder(false);
153163
if (resolvedCompletionItem === null) {
154-
return;
164+
return resolvedCompletionItem;
155165
}
156166
this._setDocumentation(resolvedCompletionItem.documentation);
157167
this._detail = resolvedCompletionItem.detail;
158168
// TODO: implement in pyls and enable with proper LSP communication
159169
// this.label = resolvedCompletionItem.label;
160170
this._resolved = true;
161-
this.connector.lab_integration.refresh_doc_panel(this);
162-
})
163-
.catch(e => {
164-
this.connector.lab_integration.set_doc_panel_placeholder(false);
165-
console.warn(e);
166171
});
167172
}
168173

packages/jupyterlab-lsp/src/features/completion/renderer.ts

Lines changed: 97 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,107 @@ import { IRenderMime } from '@jupyterlab/rendermime';
88
import { ILSPLogConsole } from '../../tokens';
99
import { LazyCompletionItem } from './item';
1010

11+
export interface ICompletionData {
12+
item: LazyCompletionItem;
13+
element: HTMLLIElement;
14+
}
15+
1116
export class LSPCompletionRenderer
1217
extends Completer.Renderer
1318
implements Completer.IRenderer {
14-
public activeChanged: Signal<LSPCompletionRenderer, LazyCompletionItem>;
19+
// signals
20+
public activeChanged: Signal<LSPCompletionRenderer, ICompletionData>;
21+
public itemShown: Signal<LSPCompletionRenderer, ICompletionData>;
22+
// observers
23+
private visibilityObserver: IntersectionObserver;
24+
private activityObserver: MutationObserver;
25+
// element data maps (with weak references for better GC)
26+
private elementToItem: WeakMap<HTMLLIElement, LazyCompletionItem>;
27+
private wasActivated: WeakMap<HTMLLIElement, boolean>;
28+
29+
protected ITEM_PLACEHOLDER_CLASS = 'lsp-detail-placeholder';
30+
protected EXTRA_INFO_CLASS = 'jp-Completer-typeExtended';
1531

1632
constructor(protected options: LSPCompletionRenderer.IOptions) {
1733
super();
1834
this.activeChanged = new Signal(this);
35+
this.itemShown = new Signal(this);
36+
this.elementToItem = new WeakMap();
37+
this.wasActivated = new WeakMap();
38+
39+
this.visibilityObserver = new IntersectionObserver(
40+
entries => {
41+
entries.forEach(entry => {
42+
if (!entry.isIntersecting) {
43+
return;
44+
}
45+
let li = entry.target as HTMLLIElement;
46+
let item = this.elementToItem.get(li);
47+
this.itemShown.emit({
48+
item: item,
49+
element: li
50+
});
51+
});
52+
},
53+
{
54+
threshold: 0.25
55+
}
56+
);
57+
58+
// note: there should be no need to unobserve deleted elements as per:
59+
// https://stackoverflow.com/a/51106262/6646912
60+
this.activityObserver = new MutationObserver(mutations => {
61+
mutations.forEach(mutation => {
62+
let li = mutation.target;
63+
if (!(li instanceof HTMLLIElement)) {
64+
return;
65+
}
66+
let inactive = !this.wasActivated.get(li);
67+
68+
if (li.classList.contains('jp-mod-active')) {
69+
if (inactive) {
70+
this.wasActivated.set(li, true);
71+
let item = this.elementToItem.get(li);
72+
this.activeChanged.emit({
73+
item: item,
74+
element: li
75+
});
76+
}
77+
} else {
78+
this.wasActivated.set(li, false);
79+
}
80+
});
81+
});
1982
}
2083

2184
protected getExtraInfo(item: LazyCompletionItem): string {
22-
switch (this.options.integrator.settings.composite.labelExtra) {
85+
const labelExtra = this.options.integrator.settings.composite.labelExtra;
86+
switch (labelExtra) {
2387
case 'detail':
24-
return item.detail;
88+
return item?.detail;
2589
case 'type':
26-
return item.type;
90+
return item?.type?.toLowerCase?.();
2791
case 'source':
28-
return item.source.name;
92+
return item?.source?.name;
2993
case 'auto':
30-
return [item.detail, item.type, item.source.name].filter(x => !!x)[0];
94+
return [
95+
item?.detail,
96+
item?.type?.toLowerCase?.(),
97+
item?.source?.name
98+
].filter(x => !!x)[0];
99+
default:
100+
this.options.console.warn(
101+
'labelExtra does not match any of the expected values',
102+
labelExtra
103+
);
104+
}
105+
}
106+
107+
public updateExtraInfo(item: LazyCompletionItem, li: HTMLLIElement) {
108+
const extraText = this.getExtraInfo(item);
109+
if (extraText) {
110+
const extraElement = li.getElementsByClassName(this.EXTRA_INFO_CLASS)[0];
111+
extraElement.textContent = extraText;
31112
}
32113
}
33114

@@ -38,31 +119,23 @@ export class LSPCompletionRenderer
38119
const li = super.createCompletionItemNode(item, orderedTypes);
39120

40121
// make sure that an instance reference, and not an object copy is being used;
41-
item = item.self;
122+
const lsp_item = item.self;
42123

43124
// only monitor nodes that have item.self as others are not our completion items
44-
if (item) {
45-
let inactive = true;
46-
const observer = new MutationObserver(mutations => {
47-
if (li.classList.contains('jp-mod-active')) {
48-
if (inactive) {
49-
inactive = false;
50-
this.activeChanged.emit(item);
51-
}
52-
} else {
53-
inactive = true;
54-
}
55-
});
56-
observer.observe(li, {
125+
if (lsp_item) {
126+
lsp_item.element = li;
127+
this.elementToItem.set(li, lsp_item);
128+
this.activityObserver.observe(li, {
57129
attributes: true,
58130
attributeFilter: ['class']
59131
});
132+
this.visibilityObserver.observe(li);
133+
// TODO: build custom li from ground up
134+
// this.updateExtraInfo(lsp_item, li)
135+
} else {
136+
this.updateExtraInfo(item, li);
60137
}
61138

62-
const extraText = this.getExtraInfo(item);
63-
const extraElement = li.querySelector('.jp-Completer-typeExtended');
64-
extraElement.textContent = extraText;
65-
66139
return li;
67140
}
68141

0 commit comments

Comments
 (0)