refactor(lsp): register per-language semantics under lsp/semantic (#1687)

Python's highlighting hooks, member intentions and Implement Methods, and PHP's construct docs move under semantic/python and semantic/php behind a small registry of language-specific behaviour. Intentions now run only for the current document's language. Behaviour is otherwise unchanged; the generic language sets stay next to their users.

Co-authored-by: muutot <[email protected]>
This commit is contained in:
Muuandmuutot authored and GitHub committed 2026-11-08 02:42:53 +08:00
1 parent dfcdd8d764
commit 200ab10e65
16 files changed
+235 -112

No files matched your search

+3 -2
View File
@@ -14,7 +14,8 @@ import { expressionTypeInfo } from "./typeInfo";
import { gotoHighlightedUsage, gotoTargets, highlightUsages } from "./navigation";
import { gotoDiagnostic, showErrorDescription } from "./diagnostics";
import { applyKindSilently, applyQuickFix, runKindAction, showIntentions } from "./intentions";
import { implementPythonAbstractMethods } from "./pythonMembers";
import { docOf } from "./lsp";
import { languageSemantics } from "./semantic";
import { workflowSettings, type ActionsOnSaveValid } from "@/platform/workflowSettings";
import { startRename } from "./rename";
import { formatDocument, reformatDialog } from "./format";
@@ -340,7 +341,7 @@ export function registerLspActions(): () => void {
title: IMPLEMENT_TITLE,
none: "Nothing to implement",
label: "Implement Members",
fallback: () => implementPythonAbstractMethods(v),
fallback: () => languageSemantics(docOf(v)?.language)?.implementMethods?.(v) ?? Promise.resolve(false),
}),
),
editorAction("code.overrideMethods", "Override Methods…", "Code", (v) =>
+2 -2
View File
@@ -56,7 +56,7 @@ import { editorPrefs } from "@/platform/editorSettings";
import { tx } from "@/platform/i18n";
import { showHint } from "./hint";
import { inplaceRenameActive } from "./inplaceRename";
import { phpConstructDocumentation } from "./phpConstructs";
import { languageSemantics } from "./semantic";
interface RequestContext {
doc: number;
@@ -369,7 +369,7 @@ async function fallback(ctx: CompletionContext): Promise<CompletionResult | null
export function makeCompletion(item: CompletionItem, req: RequestContext): LspCompletion {
if (item.kind === "keyword" && isEmptyMarkup(item.documentation)) {
const value = phpConstructDocumentation(req.language, item.label);
const value = languageSemantics(req.language)?.constructDocumentation?.(item.label);
if (value) item = { ...item, documentation: { kind: "markdown", value } };
}
// The lookup handles commit characters itself (focused lookups only, see `lookupKeys`).
+9 -4
View File
@@ -35,10 +35,15 @@ import { applyCodeAction, quickFixesAt } from "./intentions";
import { editorPrefs } from "@/platform/editorSettings";
import { CONTRIBUTED_HOVER, contributedAt, type ContributedHover } from "@/editor/api";
import { navigationModifierHeld, onNavigationModifier, pointerTarget } from "@/platform/navigationModifier";
import { phpConstructAt } from "./phpConstructs";
import { languageSemantics } from "./semantic";
let hoverRequest: RequestId | null = null;
/** Documentation of a language construct (PHP's `empty`) at `pos`, via the language's registration. */
function constructAt(state: EditorView["state"], pos: number, language: string | null | undefined) {
return languageSemantics(language)?.constructAt?.(state, pos) ?? null;
}
export async function requestHover(view: EditorView, pos: number): Promise<Hover | null> {
const own = contributedAt<ContributedHover>(view.state, CONTRIBUTED_HOVER, pos);
if (own) {
@@ -49,7 +54,7 @@ export async function requestHover(view: EditorView, pos: number): Promise<Hover
}
const doc = docOf(view);
if (!doc) return null;
const construct = phpConstructAt(view.state, pos, doc.language);
const construct = constructAt(view.state, pos, doc.language);
if (construct)
return {
contents: { kind: "markdown", value: construct.markdown },
@@ -238,7 +243,7 @@ function showDoc(view: EditorView, entry: DocEntry, at: number) {
}
export function showPhpConstructDocumentation(view: EditorView, pos: number): boolean {
const construct = phpConstructAt(view.state, pos, docOf(view)?.language ?? null);
const construct = constructAt(view.state, pos, docOf(view)?.language);
if (!construct) return false;
showDoc(view, { markup: { kind: "markdown", value: construct.markdown }, language: "PHP" }, construct.from);
return true;
@@ -278,7 +283,7 @@ export async function quickDocumentation(view: EditorView) {
markup: hover.contents,
language: ctx?.uiMode ?? null,
source:
ctx && !phpConstructAt(view.state, pos, ctx.language)
ctx && !constructAt(view.state, pos, ctx.language)
? definitionOf(ctx.doc, offsetToPos(view.state.doc, pos))
: undefined,
},
+7 -2
View File
@@ -21,7 +21,7 @@ import { debounce, docOf, isUnavailable, lspRequest, reportLspError } from "./ls
import { offsetsToRange } from "./positions";
import { showHint } from "./hint";
import { formatLines } from "./format";
import { pythonMemberIntentions } from "./pythonMembers";
import { languageSemantics } from "./semantic";
import { caretAnchor, openPopup, type ListPopupItem, type ListPreview } from "./popupStore";
import { editPreviewLines } from "./workspaceEdit";
import { featureBackoff } from "./availability";
@@ -694,5 +694,10 @@ class FloatingBulb {
const floatingBulb = ViewPlugin.fromClass(FloatingBulb);
export function intentionsExtension(): Extension {
return [bulbField, bulbPlugin, floatingBulb, intentionProviders.of(pythonMemberIntentions)];
return [
bulbField,
bulbPlugin,
floatingBulb,
intentionProviders.of((view) => languageSemantics(docOf(view)?.language)?.intentions?.(view) ?? []),
];
}
@@ -13,8 +13,8 @@ vi.mock("@/platform/ipc", async (orig) => {
return { ...actual, isTauri: () => true, commands: { ...actual.commands, lspSemanticTokens } };
});
const { semanticExtension } = await import("./semantic");
const { lspDoc } = await import("./lsp");
const { semanticExtension } = await import("./index");
const { lspDoc } = await import("../lsp");
const views: EditorView[] = [];
afterEach(() => {
@@ -8,22 +8,26 @@
* identifiers): local variables and parameters additionally get
* `cm-sem-rainbow-<n>`, `n` derived from the name, so one name keeps one
* colour (`--syn-rainbow-<n>`) throughout the file.
*
* Language-specific behaviour lives in the `python/` and `php/` folders,
* registered into `languages.ts`; everything here is language-agnostic.
*/
import { syntaxTree } from "@codemirror/language";
import type { Tree } from "@lezer/common";
import {
pythonSyntaxRanges,
pythonFallbackRanges,
pythonSymbolClasses,
pythonSymbolKinds,
type PythonSymbolKind,
} from "./pythonHighlight";
import { Prec, StateEffect, StateField, type Extension, type Text } from "@codemirror/state";
import { Decoration, EditorView, ViewPlugin, type DecorationSet, type ViewUpdate } from "@codemirror/view";
import { commands, type SemanticTokens } from "@/platform/ipc";
import { editorPrefs } from "@/platform/editorSettings";
import { debounce, docOf, isUnavailable, lspRequest } from "./lsp";
import { featureBackoff, onLspRefresh } from "./availability";
import { debounce, docOf, isUnavailable, lspRequest } from "../lsp";
import { featureBackoff, onLspRefresh } from "../availability";
import { languageSemantics, type SemanticRange } from "./languages";
// Language folders register their semantics by importing this module; lookups
// elsewhere go through the re-exported `languageSemantics`.
export { languageSemantics } from "./languages";
// Registers each language's semantics (highlighting hooks, construct docs).
import "./python";
import "./php";
const semanticBackoff = featureBackoff();
@@ -89,33 +93,6 @@ export function rainbowIndex(name: string): number {
/** Locals and parameters only: no fields, statics, constants, library or `self` names. */
const RAINBOW_TYPES = new Set(["variable", "parameter"]);
const NOT_LOCAL = new Set(["static", "constant", "defaultLibrary", "global", "classScope", "fileScope"]);
const PYTHON_VARIABLE_KINDS = new Set<PythonSymbolKind>([
"localVariable",
"globalVariable",
"instanceAttribute",
"classAttribute",
"constant",
"property",
"typeParameter",
"unusedParameter",
]);
const PYTHON_SYMBOL_BASES = new Set([
"cm-sem-variable",
"cm-sem-global",
"cm-sem-property",
"cm-sem-constant",
"cm-sem-type",
"cm-sem-typeParameter",
"cm-sem-parameter",
"cm-sem-builtin",
]);
export interface SemanticRange {
from: number;
to: number;
className: string;
source?: "pythonSyntax" | "pythonFallback";
}
function rangeCoverage(ranges: Array<{ from: number; to: number }>) {
ranges.sort((a, b) => a.from - b.from || a.to - b.to);
@@ -141,8 +118,9 @@ export function semanticRanges(
language?: string,
tree?: Tree,
): SemanticRange[] {
const symbols = language === "python" && tree ? pythonSymbolKinds(tree, doc) : undefined;
const syntax = language === "python" && tree ? pythonSyntaxRanges(tree, doc, symbols) : [];
const semantics = languageSemantics(language);
const symbols = semantics?.symbolKinds && tree ? semantics.symbolKinds(tree, doc) : undefined;
const syntax = semantics?.syntaxRanges && tree ? semantics.syntaxRanges(tree, doc, symbols ?? new Map()) : [];
const covered = rangeCoverage(syntax);
const serverRanges: Array<{ from: number; to: number }> = [];
const out: SemanticRange[] = [];
@@ -161,7 +139,7 @@ export function semanticRanges(
const to = Math.min(from + len, l.to);
if (from >= to) continue;
const type = tokenTypes[typeIdx] ?? "";
const pythonKind = symbols?.get(`${from}:${to}`);
const kind = symbols?.get(`${from}:${to}`);
const names: string[] = [];
for (let b = 0; b < tokenModifiers.length; b++) if (mods & (1 << b)) names.push(tokenModifiers[b]!);
const classes: string[] = [];
@@ -174,22 +152,14 @@ export function semanticRanges(
if (base && base !== "variable") classes.push(`cm-sem-${base}`);
// `hasOwn`: servers send names like `constructor` (jdtls) that `Object.prototype` also has.
for (const n of names) if (Object.hasOwn(MODIFIER_CLASS, n)) classes.push(`cm-sem-${MODIFIER_CLASS[n]}`);
if (language === "python" && names.includes("static")) classes.push("cm-sem-python-static");
if (language === "python" && type === "method") classes.push("cm-sem-python-method-call");
if (language === "python" && names.includes("defaultLibrary")) classes.push("cm-sem-python-builtin");
if (pythonKind && PYTHON_VARIABLE_KINDS.has(pythonKind)) {
for (let j = classes.length - 1; j >= 0; j--) if (PYTHON_SYMBOL_BASES.has(classes[j]!)) classes.splice(j, 1);
classes.push(...pythonSymbolClasses(pythonKind).split(" "));
}
semantics?.tokenClasses?.({ type, modifiers: names, kind, classes });
const local =
rainbow &&
!constant &&
!names.includes("selfParameter") &&
!names.some((n) => NOT_LOCAL.has(n)) &&
(language === "python"
? pythonKind
? pythonKind === "localVariable" || pythonKind === "parameter"
: type === "parameter"
(semantics?.isLocal
? semantics.isLocal({ type, modifiers: names, kind })
: RAINBOW_TYPES.has(type) && (type === "parameter" || !marksLocals || names.includes("local")));
serverRanges.push({ from, to });
if (!classes.length && !local) continue;
@@ -197,15 +167,17 @@ export function semanticRanges(
if (local) classes.push(`cm-sem-rainbow-${rainbowIndex(doc.sliceString(from, to))}`);
out.push({ from, to, className: classes.join(" ") });
}
if (language === "python" && tree) {
if (semantics?.fallbackRanges && tree) {
const serverCovered = rangeCoverage(serverRanges);
out.push(
...pythonFallbackRanges(
tree,
doc,
rainbow ? (name) => `cm-sem-rainbow-${rainbowIndex(name)}` : undefined,
symbols,
).filter((r) => !covered(r.from, r.to) && !serverCovered(r.from, r.to)),
...semantics
.fallbackRanges(
tree,
doc,
rainbow ? (name) => `cm-sem-rainbow-${rainbowIndex(name)}` : undefined,
symbols ?? new Map(),
)
.filter((r) => !covered(r.from, r.to) && !serverCovered(r.from, r.to)),
);
}
out.push(...syntax);
@@ -213,11 +185,11 @@ export function semanticRanges(
}
const markCache = new Map<string, Decoration>();
const markFor = (cls: string, source?: "pythonSyntax" | "pythonFallback", symbol?: string) => {
const markFor = (cls: string, source?: SemanticRange["source"], symbol?: string) => {
if (symbol !== undefined) return Decoration.mark({ class: cls, symbol });
const key = `${source ?? "semantic"}:${cls}`;
let m = markCache.get(key);
if (!m) markCache.set(key, (m = Decoration.mark({ class: cls, pythonSyntax: source !== undefined })));
if (!m) markCache.set(key, (m = Decoration.mark({ class: cls, languageSyntax: source !== undefined })));
return m;
};
@@ -270,15 +242,15 @@ const semanticPlugin = ViewPlugin.fromClass(
}
refreshSyntax(doc: Text) {
const tree = syntaxTree(this.view.state);
const python = docOf(this.view)?.language === "python";
const symbols = python ? pythonSymbolKinds(tree, doc) : undefined;
const syntax = python ? pythonSyntaxRanges(tree, doc, symbols) : [];
const semantics = languageSemantics(docOf(this.view)?.language);
const symbols = semantics?.symbolKinds ? semantics.symbolKinds(tree, doc) : undefined;
const syntax = semantics?.syntaxRanges ? semantics.syntaxRanges(tree, doc, symbols ?? new Map()) : [];
const covered = rangeCoverage(syntax);
const retained: Array<{ from: number; to: number }> = [];
const marks = syntax.map((r) => markFor(r.className, r.source).range(r.from, r.to));
this.view.state.field(semanticField).between(0, doc.length, (from, to, decoration) => {
if (
!decoration.spec.pythonSyntax &&
!decoration.spec.languageSyntax &&
!covered(from, to) &&
decoration.spec.symbol === doc.sliceString(from, to)
) {
@@ -286,13 +258,13 @@ const semanticPlugin = ViewPlugin.fromClass(
retained.push({ from, to });
}
});
if (docOf(this.view)?.language === "python") {
if (semantics?.fallbackRanges) {
const retainedCovered = rangeCoverage(retained);
for (const r of pythonFallbackRanges(
for (const r of semantics.fallbackRanges(
tree,
doc,
this.rainbow ? (name) => `cm-sem-rainbow-${rainbowIndex(name)}` : undefined,
symbols,
symbols ?? new Map(),
))
if (!covered(r.from, r.to) && !retainedCovered(r.from, r.to))
marks.push(markFor(r.className, r.source).range(r.from, r.to));
+73
View File
@@ -0,0 +1,73 @@
/**
* Per-language semantics behind `lsp/semantic`: each language folder
* (`python/`, `php/`) registers a provider here, and feature code looks it up
* by the document's language instead of branching on language names.
*/
import type { EditorState, Text } from "@codemirror/state";
import type { EditorView } from "@codemirror/view";
import type { Tree } from "@lezer/common";
import type { ContributedIntention } from "@/editor/api";
/** One decorated range; `source` says which parse-tree pass produced it. */
export interface SemanticRange {
from: number;
to: number;
className: string;
source?: "syntax" | "fallback";
}
/** A server token, with the classes the generic mapping already produced. */
export interface SemanticToken {
type: string;
modifiers: readonly string[];
/** Kind from the language's own `symbolKinds` classification, when present. */
kind: string | undefined;
/** Classes accumulated so far; the hook appends or removes. */
classes: string[];
}
/** Documentation of a language construct that has no source declaration. */
export interface ConstructDoc {
from: number;
to: number;
markdown: string;
}
export interface LanguageSemantics {
/** Classifies identifiers from the parse tree (`"from:to"` → kind). */
symbolKinds?(tree: Tree, doc: Text): Map<string, string>;
/** Parse-tree ranges styling what the LSP tokens do not cover. */
syntaxRanges?(tree: Tree, doc: Text, symbols: Map<string, string>): SemanticRange[];
/** Fast local guesses shown while the server is pending. */
fallbackRanges?(
tree: Tree,
doc: Text,
rainbow: ((name: string) => string) | undefined,
symbols: Map<string, string>,
): SemanticRange[];
/** Extra classes for a server token, on top of the generic ones. */
tokenClasses?(token: SemanticToken): void;
/** Whether a server token counts as a local for rainbow colouring; default: the generic rule. */
isLocal?(token: Pick<SemanticToken, "type" | "modifiers" | "kind">): boolean;
/** Documentation of the language construct called at `pos`, if there is one. */
constructAt?(state: EditorState, pos: number): ConstructDoc | null;
/** Construct documentation by name (completion item detail). */
constructDocumentation?(name: string): string | null;
/** Bulb intentions contributed on this language's documents. */
intentions?(view: EditorView): ContributedIntention[];
/** Fallback for the Implement Methods action when the server offers no code action. */
implementMethods?(view: EditorView): Promise<boolean>;
}
const registry: Record<string, LanguageSemantics> = {};
export function registerLanguageSemantics(languages: string | readonly string[], semantics: LanguageSemantics): void {
for (const language of typeof languages === "string" ? [languages] : languages) {
const id = language.toLowerCase();
registry[id] = { ...registry[id], ...semantics };
}
}
export function languageSemantics(language: string | null | undefined): LanguageSemantics | undefined {
return language ? registry[language.toLowerCase()] : undefined;
}
@@ -2,11 +2,11 @@ import { afterEach, describe, expect, it } from "vitest";
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { languages } from "@codemirror/language-data";
import { phpConstructAt, phpConstructDocumentation } from "./phpConstructs";
import { lspDoc } from "./lsp";
import { quickDefinition, requestHover } from "./hover";
import { gotoTargets } from "./navigation";
import { closePopup, useLspPopup } from "./popupStore";
import { phpConstructAt, phpConstructDocumentation } from "./constructs";
import { lspDoc } from "../../lsp";
import { quickDefinition, requestHover } from "../../hover";
import { gotoTargets } from "../../navigation";
import { closePopup, useLspPopup } from "../../popupStore";
const php = await languages.find((l) => l.name === "PHP")!.load();
const views: EditorView[] = [];
File renamed without changes.
+15
View File
@@ -0,0 +1,15 @@
/**
* PHP: no semantic-token extras, but its language constructs (`empty`, `isset`)
* have no source declaration, so hover and completion get their documentation
* from here.
*/
import type { LanguageSemantics } from "../languages";
import { registerLanguageSemantics } from "../languages";
import { phpConstructAt, phpConstructDocumentation } from "./constructs";
const phpSemantics: LanguageSemantics = {
constructAt: (state, pos) => phpConstructAt(state, pos, "php"),
constructDocumentation: (name) => phpConstructDocumentation("php", name),
};
registerLanguageSemantics("php", phpSemantics);
@@ -1,9 +1,9 @@
import { describe, expect, it } from "vitest";
import { EditorState } from "@codemirror/state";
import { ensureSyntaxTree, syntaxTree } from "@codemirror/language";
import { findLanguageDescription, loadSupport } from "../core/cm/languages";
import { pythonSyntaxRanges, pythonFallbackRanges, pythonSymbolKinds } from "./pythonHighlight";
import { semanticRanges } from "./semantic";
import { findLanguageDescription, loadSupport } from "../../../core/cm/languages";
import { pythonSyntaxRanges, pythonFallbackRanges, pythonSymbolKinds } from "./highlight";
import { semanticRanges } from "../index";
async function parse(source: string) {
const support = await loadSupport(findLanguageDescription("Python", "demo.py")!);
@@ -1,12 +1,6 @@
import type { SyntaxNode, Tree } from "@lezer/common";
import type { Text } from "@codemirror/state";
interface PythonRange {
from: number;
to: number;
className: string;
source: "pythonSyntax" | "pythonFallback";
}
import type { SemanticRange } from "../languages";
export type PythonSymbolKind =
| "function"
@@ -478,8 +472,8 @@ export function pythonSyntaxRanges(
tree: Tree,
doc: Text,
symbols: Map<string, PythonSymbolKind> = pythonSymbolKinds(tree, doc),
): PythonRange[] {
const ranges: PythonRange[] = [];
): SemanticRange[] {
const ranges: SemanticRange[] = [];
tree.iterate({
enter(cursor) {
const node = cursor.node;
@@ -505,10 +499,10 @@ export function pythonSyntaxRanges(
let from = node.from;
for (let child = node.firstChild; child; child = child.nextSibling) {
if (child.name !== "FormatReplacement") continue;
if (child.from > from) ranges.push({ from, to: child.from, className, source: "pythonSyntax" });
if (child.from > from) ranges.push({ from, to: child.from, className, source: "syntax" });
from = child.to;
}
if (from < node.to) ranges.push({ from, to: node.to, className, source: "pythonSyntax" });
if (from < node.to) ranges.push({ from, to: node.to, className, source: "syntax" });
return;
}
const text = doc.sliceString(node.from, node.to);
@@ -555,7 +549,7 @@ export function pythonSyntaxRanges(
} else if (node.name === "Escape") style = "python-escape";
else if (node.name === "type" && node.parent?.name === "TypeDefinition") style = "keyword";
if (style) className = `cm-sem-${style}`;
if (className) ranges.push({ from: node.from, to: node.to, className, source: "pythonSyntax" });
if (className) ranges.push({ from: node.from, to: node.to, className, source: "syntax" });
if (style === "python-docstring") return false;
},
});
@@ -574,8 +568,8 @@ export function pythonFallbackRanges(
doc: Text,
rainbow?: (name: string) => string,
symbols: Map<string, PythonSymbolKind> = pythonSymbolKinds(tree, doc),
): PythonRange[] {
const ranges: PythonRange[] = [];
): SemanticRange[] {
const ranges: SemanticRange[] = [];
tree.iterate({
enter(cursor) {
const node = cursor.node;
@@ -592,7 +586,7 @@ export function pythonFallbackRanges(
className:
pythonSymbolClasses(kind) +
(local && rainbow && name !== "self" && name !== "cls" ? ` ${rainbow(name)}` : ""),
source: "pythonFallback",
source: "fallback",
});
},
});
+58
View File
@@ -0,0 +1,58 @@
/**
* Python's semantic highlighting beyond the LSP tokens (PyCharm's category
* colours): symbol kinds from the parse tree, syntax ranges the server never
* reports, and fallbacks while the server is pending.
*/
import type { LanguageSemantics } from "../languages";
import { registerLanguageSemantics } from "../languages";
import {
pythonFallbackRanges,
pythonSymbolClasses,
pythonSymbolKinds,
pythonSyntaxRanges,
type PythonSymbolKind,
} from "./highlight";
import { implementPythonAbstractMethods, pythonMemberIntentions } from "./members";
/** Server tokens these kinds override: the kind's own classes replace the generic ones. */
const VARIABLE_KINDS = new Set<PythonSymbolKind>([
"localVariable",
"globalVariable",
"instanceAttribute",
"classAttribute",
"constant",
"property",
"typeParameter",
"unusedParameter",
]);
const SYMBOL_BASES = new Set([
"cm-sem-variable",
"cm-sem-global",
"cm-sem-property",
"cm-sem-constant",
"cm-sem-type",
"cm-sem-typeParameter",
"cm-sem-parameter",
"cm-sem-builtin",
]);
const pythonSemantics: LanguageSemantics = {
symbolKinds: pythonSymbolKinds,
syntaxRanges: (tree, doc, symbols) => pythonSyntaxRanges(tree, doc, symbols as Map<string, PythonSymbolKind>),
fallbackRanges: (tree, doc, rainbow, symbols) =>
pythonFallbackRanges(tree, doc, rainbow, symbols as Map<string, PythonSymbolKind>),
tokenClasses({ type, modifiers, kind, classes }) {
if (modifiers.includes("static")) classes.push("cm-sem-python-static");
if (type === "method") classes.push("cm-sem-python-method-call");
if (modifiers.includes("defaultLibrary")) classes.push("cm-sem-python-builtin");
if (kind && VARIABLE_KINDS.has(kind as PythonSymbolKind)) {
for (let j = classes.length - 1; j >= 0; j--) if (SYMBOL_BASES.has(classes[j]!)) classes.splice(j, 1);
classes.push(...pythonSymbolClasses(kind as PythonSymbolKind).split(" "));
}
},
isLocal: ({ type, kind }) => (kind ? kind === "localVariable" || kind === "parameter" : type === "parameter"),
intentions: pythonMemberIntentions,
implementMethods: implementPythonAbstractMethods,
};
registerLanguageSemantics("python", pythonSemantics);
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { classMembers, enclosingClass, memberStubs } from "./pythonMembers";
import { classMembers, enclosingClass, memberStubs } from "./members";
const src = `from abc import ABC, abstractmethod
@@ -8,10 +8,10 @@ import { indentUnit } from "@codemirror/language";
import type { EditorView } from "@codemirror/view";
import { commands } from "@/platform/ipc";
import type { ContributedIntention } from "@/editor/api";
import { docOf, lspRequest, reportLspError } from "./lsp";
import { offsetToPos } from "./positions";
import { readFileLines } from "./files";
import { showHint } from "./hint";
import { docOf, lspRequest, reportLspError } from "../../lsp";
import { offsetToPos } from "../../positions";
import { readFileLines } from "../../files";
import { showHint } from "../../hint";
const CLASS_HEADER = /^(\s*)class\s+\w+\s*\((.*)\)\s*:/;
const CLASS = /^\s*class\s/;
@@ -1,18 +1,18 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { findLanguageDescription, loadSupport } from "../core/cm/languages";
import { findLanguageDescription, loadSupport } from "../../core/cm/languages";
import type { SemanticTokens } from "@/platform/ipc";
import type { EditorDocContext } from "@/editor/api";
import { lspDoc } from "./lsp";
import { semanticExtension } from "./semantic";
import { lspDoc } from "../lsp";
import { semanticExtension } from "./index";
const requests = vi.hoisted(() => [] as Array<(tokens: SemanticTokens) => void>);
vi.mock("./lsp", async (importOriginal) => ({
...(await importOriginal<typeof import("./lsp")>()),
vi.mock("../lsp", async (importOriginal) => ({
...(await importOriginal<typeof import("../lsp")>()),
lspRequest: () => new Promise<SemanticTokens>((resolve) => requests.push(resolve)),
}));
vi.mock("./availability", () => ({
vi.mock("../availability", () => ({
featureBackoff: () => ({ active: () => false, mark: () => {} }),
onLspRefresh: () => () => {},
}));