Skip to content

Commit 7267964

Browse files
deduplicate python hovers and completions in core (cherry-pick from release/2025.12 branch) (#10977)
Cherry-pick #10955 onto the main branch.
1 parent 1fc5a45 commit 7267964

File tree

7 files changed

+117
-6
lines changed

7 files changed

+117
-6
lines changed

extensions/positron-python/python_files/posit/positron/positron_jedilsp.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,6 @@ def lsp_initialize(self, params: InitializeParams) -> InitializeResult:
303303

304304
# Remove LSP features that are redundant with Pyrefly.
305305
features_to_remove = [
306-
"textDocument/hover",
307-
"textDocument/signatureHelp",
308306
"textDocument/declaration",
309307
"textDocument/definition",
310308
"textDocument/typeDefinition",

src/vs/editor/common/languages.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,14 @@ export interface Hover {
193193
* Can decrease the verbosity of the hover
194194
*/
195195
canDecreaseVerbosity?: boolean;
196+
197+
// --- Start Positron ---
198+
/**
199+
* Id of the extension that provided this hover.
200+
* Used to filter duplicate hovers from certain extensions.
201+
*/
202+
extensionId?: string;
203+
// --- End Positron ---
196204
}
197205

198206
/**

src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ import { IHoverService, WorkbenchHoverDelegate } from '../../../../platform/hove
3131
import { AsyncIterableProducer } from '../../../../base/common/async.js';
3232
import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js';
3333
import { getHoverProviderResultsAsAsyncIterable } from './getHover.js';
34+
// --- Start Positron ---
35+
// eslint-disable-next-line no-duplicate-imports
36+
import { HoverProviderResult } from './getHover.js';
37+
// --- End Positron ---
3438
import { ICommandService } from '../../../../platform/commands/common/commands.js';
3539
import { HoverStartSource } from './hoverOperation.js';
3640
import { ScrollEvent } from '../../../../base/common/scrollable.js';
@@ -168,13 +172,29 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant<Markdow
168172
const position = anchor.range.getStartPosition();
169173
const hoverProviderResults = getHoverProviderResultsAsAsyncIterable(hoverProviderRegistry, model, position, token);
170174

175+
// --- Start Positron ---
176+
// Collect all hover results first so we can filter out Python extension hovers when other hovers exist
177+
// (to avoid redundant hover content from other Python LSP extensions)
178+
const allResults: { item: HoverProviderResult; hover: MarkdownHover }[] = [];
171179
for await (const item of hoverProviderResults) {
172180
if (!isEmptyMarkdownString(item.hover.contents)) {
173181
const range = item.hover.range ? Range.lift(item.hover.range) : anchor.range;
174182
const hoverSource = new HoverSource(item.hover, item.provider, position);
175-
yield new MarkdownHover(this, range, item.hover.contents, false, item.ordinal, hoverSource);
183+
const markdownHover = new MarkdownHover(this, range, item.hover.contents, false, item.ordinal, hoverSource);
184+
allResults.push({ item, hover: markdownHover });
176185
}
177186
}
187+
188+
const msPythonExtensionId = 'ms-python.python';
189+
const hasNonMsPythonHovers = allResults.some(r => r.item.hover.extensionId !== msPythonExtensionId);
190+
const hasMsPythonHovers = allResults.some(r => r.item.hover.extensionId === msPythonExtensionId);
191+
for (const result of allResults) {
192+
if (hasNonMsPythonHovers && hasMsPythonHovers && result.item.hover.extensionId === msPythonExtensionId) {
193+
continue;
194+
}
195+
yield result.hover;
196+
}
197+
// --- End Positron ---
178198
}
179199

180200
public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: MarkdownHover[]): IRenderedHoverParts<MarkdownHover> {

src/vs/editor/contrib/suggest/browser/suggest.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,40 @@ export async function provideSuggestionItems(
326326
return Promise.reject(new CancellationError());
327327
}
328328

329+
// --- Start Positron ---
330+
// Deduplicate completion items that have the same insertText.
331+
// When duplicates are found:
332+
// - If one item is from our Python extension and the other is not, prefer the one not from our Python extension.
333+
// - If both items are from our Python extension, the first-seen item is kept.
334+
// - If both items are from other extensions, the first-seen item is kept.
335+
const msPythonExtensionId = 'ms-python.python';
336+
const deduplicatedResult: CompletionItem[] = [];
337+
const seen = new Map<string, CompletionItem>();
338+
339+
for (const item of result) {
340+
const key = item.completion.insertText;
341+
const existing = seen.get(key);
342+
if (existing) {
343+
// If the existing item is from ms-python.python and the new one is not,
344+
// replace with the new one
345+
if (existing.extensionId?.value === msPythonExtensionId &&
346+
item.extensionId?.value !== msPythonExtensionId) {
347+
const existingIndex = deduplicatedResult.indexOf(existing);
348+
if (existingIndex !== -1) {
349+
deduplicatedResult[existingIndex] = item;
350+
seen.set(key, item);
351+
}
352+
}
353+
} else {
354+
// Otherwise, keep the existing item (skip the duplicate)
355+
seen.set(key, item);
356+
deduplicatedResult.push(item);
357+
}
358+
}
359+
329360
return new CompletionItemModel(
330-
result.sort(getSuggestionComparator(options.snippetSortOrder)),
361+
deduplicatedResult.sort(getSuggestionComparator(options.snippetSortOrder)),
362+
// --- End Positron ---
331363
needsClipboard,
332364
{ entries: durations, elapsed: sw.elapsed() },
333365
disposables,

src/vs/monaco.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7229,6 +7229,11 @@ declare namespace monaco.languages {
72297229
* Can decrease the verbosity of the hover
72307230
*/
72317231
canDecreaseVerbosity?: boolean;
7232+
/**
7233+
* Id of the extension that provided this hover.
7234+
* Used to filter duplicate hovers from certain extensions.
7235+
*/
7236+
extensionId?: string;
72327237
}
72337238

72347239
/**

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1863,6 +1863,12 @@ export interface HoverWithId extends languages.Hover {
18631863
* Id of the hover
18641864
*/
18651865
id: number;
1866+
// --- Start Positron ---
1867+
/**
1868+
* Id of the extension that provided this hover
1869+
*/
1870+
extensionId?: string;
1871+
// --- End Positron ---
18661872
}
18671873

18681874
// -- extension host

src/vs/workbench/api/common/extHostLanguageFeatures.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ import { IInlineCompletionsUnificationState } from '../../services/inlineComplet
4242

4343
// --- Start Positron ---
4444
import type * as positron from 'positron';
45+
46+
/**
47+
* Extracts plain text from a markdown string by removing code block syntax.
48+
*/
49+
function extractPlainTextFromMarkdown(text: string): string {
50+
return text
51+
.trim()
52+
.replace(/^```\w*\n?/gm, '')
53+
.replace(/```$/gm, '')
54+
.trim();
55+
}
4556
// --- End Positron ---
4657

4758
// --- adapter
@@ -276,6 +287,9 @@ class HoverAdapter {
276287
constructor(
277288
private readonly _documents: ExtHostDocuments,
278289
private readonly _provider: vscode.HoverProvider,
290+
// --- Start Positron ---
291+
private readonly _extensionId: string,
292+
// --- End Positron ---
279293
) { }
280294

281295
async provideHover(resource: URI, position: IPosition, context: languages.HoverContext<{ id: number }> | undefined, token: CancellationToken): Promise<extHostProtocol.HoverWithId | undefined> {
@@ -298,6 +312,18 @@ class HoverAdapter {
298312
if (!value || isFalsyOrEmpty(value.contents)) {
299313
return undefined;
300314
}
315+
// --- Start Positron ---
316+
// Filter out hovers that only contain "Unknown" text (possibly in a markdown code block).
317+
// This is because that's the literal message Pyrefly sends when it has no useful hover info to provide.
318+
const contentsText = extractPlainTextFromMarkdown(
319+
value.contents
320+
.map(c => typeof c === 'string' ? c : typeof c === 'object' && 'value' in c ? c.value : '')
321+
.join('')
322+
);
323+
if (contentsText === 'Unknown') {
324+
return undefined;
325+
}
326+
// --- End Positron ---
301327
if (!value.range) {
302328
value.range = doc.getWordRangeAtPosition(pos);
303329
}
@@ -315,7 +341,10 @@ class HoverAdapter {
315341
this._hoverCounter += 1;
316342
const hover: extHostProtocol.HoverWithId = {
317343
...convertedHover,
318-
id
344+
id,
345+
// --- Start Positron ---
346+
extensionId: this._extensionId,
347+
// --- End Positron ---
319348
};
320349
return hover;
321350
}
@@ -1568,6 +1597,16 @@ class SignatureHelpAdapter {
15681597

15691598
const value = await this._provider.provideSignatureHelp(doc, pos, token, vscodeContext);
15701599
if (value) {
1600+
// --- Start Positron ---
1601+
// Filter out signature help that only contains "Unknown" text (possibly in a markdown code block).
1602+
// This is because that's the literal message Pyrefly sends when it has no useful info to provide.
1603+
const allSignaturesUnknown = value.signatures.length > 0 && value.signatures.every(sig =>
1604+
extractPlainTextFromMarkdown(sig.label) === 'Unknown'
1605+
);
1606+
if (allSignaturesUnknown) {
1607+
return undefined;
1608+
}
1609+
// --- End Positron ---
15711610
const id = this._cache.add([value]);
15721611
return { ...typeConvert.SignatureHelp.from(value), id };
15731612
}
@@ -2383,7 +2422,10 @@ export class ExtHostLanguageFeatures extends CoreDisposable implements extHostPr
23832422
// --- extra info
23842423

23852424
registerHoverProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.HoverProvider, extensionId?: ExtensionIdentifier): vscode.Disposable {
2386-
const handle = this._addNewAdapter(new HoverAdapter(this._documents, provider), extension);
2425+
// --- Start Positron ---
2426+
// added extensionId parameter to HoverAdapter
2427+
const handle = this._addNewAdapter(new HoverAdapter(this._documents, provider, extension.identifier.value), extension);
2428+
// --- End Positron ---
23872429
this._proxy.$registerHoverProvider(handle, this._transformDocumentSelector(selector, extension));
23882430
return this._createDisposable(handle);
23892431
}

0 commit comments

Comments
 (0)