mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 10:16:03 -10:00
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:
1 parent
dfcdd8d764
commit
200ab10e65
16 files changed
+235
-112
No files matched your search
@@ -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) =>
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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));
|
||||
@@ -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;
|
||||
}
|
||||
+5
-5
@@ -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.
@@ -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);
|
||||
+3
-3
@@ -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",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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
-1
@@ -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: () => () => {},
|
||||
}));
|
||||
Reference in new issue
Block a user