修代码补全
This commit is contained in:
@@ -9,6 +9,10 @@ import {
|
||||
Ra3ReferenceProvider,
|
||||
} from "./features/navigation";
|
||||
import { Ra3Diagnostics } from "./features/diagnostics";
|
||||
import {
|
||||
Ra3SemanticTokensProvider,
|
||||
RA3_SEMANTIC_TOKENS_LEGEND,
|
||||
} from "./features/semanticTokens";
|
||||
|
||||
const XML_SELECTOR: vscode.DocumentSelector = [{ language: "xml" }];
|
||||
|
||||
@@ -55,6 +59,13 @@ export function activate(context: vscode.ExtensionContext): void {
|
||||
new Ra3DocumentSymbolProvider(),
|
||||
),
|
||||
);
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerDocumentSemanticTokensProvider(
|
||||
XML_SELECTOR,
|
||||
new Ra3SemanticTokensProvider(),
|
||||
RA3_SEMANTIC_TOKENS_LEGEND,
|
||||
),
|
||||
);
|
||||
|
||||
const diagnostics = new Ra3Diagnostics(ws);
|
||||
context.subscriptions.push(diagnostics);
|
||||
|
||||
+28
-11
@@ -1,6 +1,10 @@
|
||||
import * as vscode from "vscode";
|
||||
import { parseXml, type XmlElement } from "../language/xmlParser";
|
||||
import { analyzeContext, type CompletionContext } from "../language/context";
|
||||
import {
|
||||
analyzeContext,
|
||||
splitListValuePrefix,
|
||||
type CompletionContext,
|
||||
} from "../language/context";
|
||||
import { resolveElementType } from "../language/typeContext";
|
||||
import * as model from "../model/schemaModel";
|
||||
import { isLocalReferenceAttribute } from "../indexer/refs";
|
||||
@@ -178,12 +182,31 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
const el = ctx.element;
|
||||
const attr = ctx.attr;
|
||||
if (!el || !attr) return [];
|
||||
const prefix = ctx.valuePrefix;
|
||||
const rawPrefix = ctx.valuePrefix;
|
||||
|
||||
const endOffset = attr.quoteEnd > attr.valueEnd ? attr.valueEnd : document.offsetAt(position);
|
||||
const attrName = attr.name.toLowerCase();
|
||||
const elType = resolveElementType(el);
|
||||
const attrInfo = model
|
||||
.attributesOfType(elType)
|
||||
.find((a) => a.name.toLowerCase() === attrName);
|
||||
|
||||
// xs:list values (bit flags such as Surfaces="GROUND WATER") are
|
||||
// whitespace-separated: only the token currently being edited is used for
|
||||
// filtering, and the replacement range covers that token instead of the
|
||||
// whole value.
|
||||
const seg = attrInfo?.isList
|
||||
? splitListValuePrefix(rawPrefix)
|
||||
: { token: rawPrefix, start: 0 };
|
||||
const prefix = seg.token;
|
||||
|
||||
const valueStartOffset =
|
||||
attr.valueStart >= 0 ? attr.valueStart : document.offsetAt(position);
|
||||
const endOffset =
|
||||
attr.quoteEnd > attr.valueEnd ? attr.valueEnd : document.offsetAt(position);
|
||||
const rangeStart = valueStartOffset + seg.start;
|
||||
const valueRange = new vscode.Range(
|
||||
document.positionAt(attr.valueStart),
|
||||
document.positionAt(Math.max(attr.valueStart, endOffset)),
|
||||
document.positionAt(rangeStart),
|
||||
document.positionAt(Math.max(rangeStart, endOffset)),
|
||||
);
|
||||
|
||||
const make = (
|
||||
@@ -201,7 +224,6 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
};
|
||||
|
||||
const isInclude = el.name === "Include";
|
||||
const attrName = attr.name.toLowerCase();
|
||||
|
||||
// Include type / source
|
||||
if (isInclude && attrName === "type") {
|
||||
@@ -218,11 +240,6 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
);
|
||||
}
|
||||
|
||||
const elType = resolveElementType(el);
|
||||
const attrInfo = model
|
||||
.attributesOfType(elType)
|
||||
.find((a) => a.name.toLowerCase() === attrName);
|
||||
|
||||
// inheritFrom: same element type first, then everything.
|
||||
if (attrName === "inheritfrom") {
|
||||
return this.assetIdItems(idx, el.name, null, prefix, make);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as vscode from "vscode";
|
||||
import { parseXml } from "../language/xmlParser";
|
||||
import { buildSemanticTokenRanges } from "../language/semanticTokens";
|
||||
|
||||
const TOKEN_TYPES = ["type", "property", "string"] as const;
|
||||
|
||||
export const RA3_SEMANTIC_TOKENS_LEGEND = new vscode.SemanticTokensLegend([
|
||||
...TOKEN_TYPES,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Highlighting fallback for malformed XML.
|
||||
*
|
||||
* While the document is well-formed, the built-in TextMate XML grammar colors
|
||||
* it as usual and this provider returns no tokens, so nothing changes. When
|
||||
* parsing reports errors (e.g. an attribute value whose closing quote has not
|
||||
* been typed yet), the TextMate structure is lost, and these semantic tokens
|
||||
* keep element names, attribute names and values colored.
|
||||
*/
|
||||
export class Ra3SemanticTokensProvider
|
||||
implements vscode.DocumentSemanticTokensProvider
|
||||
{
|
||||
async provideDocumentSemanticTokens(
|
||||
document: vscode.TextDocument,
|
||||
_token: vscode.CancellationToken,
|
||||
): Promise<vscode.SemanticTokens> {
|
||||
const text = document.getText();
|
||||
const doc = parseXml(text);
|
||||
if (doc.errors.length === 0) {
|
||||
return new vscode.SemanticTokens(new Uint32Array(0));
|
||||
}
|
||||
const ranges = buildSemanticTokenRanges(doc, text);
|
||||
const builder = new vscode.SemanticTokensBuilder(RA3_SEMANTIC_TOKENS_LEGEND);
|
||||
for (const r of ranges) {
|
||||
builder.push(
|
||||
new vscode.Range(r.line, r.startChar, r.line, r.startChar + r.length),
|
||||
r.tokenType,
|
||||
);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
+26
-1
@@ -76,7 +76,15 @@ function analyzeStartTag(
|
||||
|
||||
// Inside an attribute value?
|
||||
for (const attr of el.attrs) {
|
||||
if (attr.hasValue && offset >= attr.quoteStart && offset <= attr.quoteEnd) {
|
||||
// An unterminated value (quoteEnd < 0) happens while the user is typing
|
||||
// the opening quote of a new attribute value; it must still be treated as
|
||||
// an attribute-value context so enum/ref/define completions show up.
|
||||
if (
|
||||
attr.hasValue &&
|
||||
attr.quoteStart >= 0 &&
|
||||
offset >= attr.quoteStart &&
|
||||
(attr.quoteEnd < 0 || offset <= attr.quoteEnd)
|
||||
) {
|
||||
const start = attr.valueStart;
|
||||
const prefix = offset > start ? text.slice(start, offset) : "";
|
||||
return {
|
||||
@@ -128,3 +136,20 @@ function empty(kind: ContextKind): CompletionContext {
|
||||
existingAttrs: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the typed prefix of a whitespace-separated list value (xs:list, e.g.
|
||||
* bit flags such as Surfaces="GROUND WATER") into the token being edited and
|
||||
* the offset of that token inside the prefix.
|
||||
*
|
||||
* "GROUND WA" -> { token: "WA", start: 7 }
|
||||
* "GROUND " -> { token: "", start: 7 }
|
||||
* "WA" -> { token: "WA", start: 0 }
|
||||
*/
|
||||
export function splitListValuePrefix(prefix: string): { token: string; start: number } {
|
||||
let start = prefix.length;
|
||||
while (start > 0 && !/\s/.test(prefix[start - 1])) {
|
||||
start--;
|
||||
}
|
||||
return { token: prefix.slice(start), start };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { LineMap, type XmlDocument } from "./xmlParser";
|
||||
|
||||
/**
|
||||
* Semantic token types used by the highlighting fallback. They are standard
|
||||
* vscode token types, so every theme already has colors for them.
|
||||
*/
|
||||
export type SemanticTokenType = "type" | "property" | "string";
|
||||
|
||||
export interface SemanticTokenRange {
|
||||
line: number;
|
||||
startChar: number;
|
||||
length: number;
|
||||
tokenType: SemanticTokenType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds semantic token ranges from a tolerant parse tree.
|
||||
*
|
||||
* This is the highlighting fallback for malformed XML: while the TextMate
|
||||
* grammar loses structure (e.g. an attribute value whose closing quote has
|
||||
* not been typed yet turns the rest of the file into one string), semantic
|
||||
* tokens keep element names, attribute names and attribute values colored.
|
||||
* The ranges are sorted by position for the vscode encoder.
|
||||
*/
|
||||
export function buildSemanticTokenRanges(
|
||||
doc: XmlDocument,
|
||||
text: string,
|
||||
): SemanticTokenRange[] {
|
||||
const lineMap = new LineMap(text);
|
||||
const out: SemanticTokenRange[] = [];
|
||||
|
||||
const push = (offset: number, length: number, tokenType: SemanticTokenType) => {
|
||||
if (length <= 0 || offset < 0 || offset + length > text.length) return;
|
||||
const pos = lineMap.positionAt(offset);
|
||||
out.push({
|
||||
line: pos.line,
|
||||
startChar: pos.character,
|
||||
length,
|
||||
tokenType,
|
||||
});
|
||||
};
|
||||
|
||||
for (const el of doc.elements) {
|
||||
// Element name in the start tag.
|
||||
push(el.start + 1, el.name.length, "type");
|
||||
// Element name in the closing tag (when present).
|
||||
if (el.closeTagStart >= 0) {
|
||||
push(el.closeTagStart + 2, el.name.length, "type");
|
||||
}
|
||||
for (const attr of el.attrs) {
|
||||
push(attr.nameStart, attr.name.length, "property");
|
||||
if (!attr.hasValue) continue;
|
||||
// Include the surrounding quotes when available; for an unterminated
|
||||
// value quoteEnd is -1 and the token ends at the recovered value end.
|
||||
const start = attr.quoteStart >= 0 ? attr.quoteStart : attr.valueStart;
|
||||
const end = attr.quoteEnd >= 0 ? attr.quoteEnd : attr.valueEnd;
|
||||
push(start, end - start, "string");
|
||||
}
|
||||
}
|
||||
|
||||
out.sort((a, b) => a.line - b.line || a.startChar - b.startChar);
|
||||
return out;
|
||||
}
|
||||
@@ -336,7 +336,16 @@ export function parseXml(text: string): XmlDocument {
|
||||
const gt = findTagEnd(text, i + 1);
|
||||
if (gt < 0) {
|
||||
err("Unterminated start tag", i);
|
||||
const content = text.slice(i + 1);
|
||||
// Recovery while typing: an attribute value whose closing quote has not
|
||||
// been typed yet makes the scanner run to EOF. End the malformed start
|
||||
// tag at the first line break (or EOF) so the rest of the document is
|
||||
// still parsed and completion/hover keep working for the elements after
|
||||
// the broken tag. The missing quote/tag end is still reported above.
|
||||
let recoverTo = i + 1;
|
||||
while (recoverTo < text.length && text[recoverTo] !== "\n" && text[recoverTo] !== "\r") {
|
||||
recoverTo++;
|
||||
}
|
||||
const content = text.slice(i + 1, recoverTo);
|
||||
const raw = parseTag(content, i + 1);
|
||||
if (raw.name) {
|
||||
const el = buildElement(raw, stack.length);
|
||||
@@ -344,7 +353,8 @@ export function parseXml(text: string): XmlDocument {
|
||||
root = root ?? el;
|
||||
stack.push(el);
|
||||
}
|
||||
break;
|
||||
i = recoverTo + 1;
|
||||
continue;
|
||||
}
|
||||
const content = text.slice(i + 1, gt);
|
||||
const raw = parseTag(content, i + 1);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -20,6 +20,8 @@ export interface AttributeInfo {
|
||||
/** True for reference-typed attributes whose simple type has no refType. */
|
||||
isRef: boolean;
|
||||
enumValues: string[];
|
||||
/** True for xs:list types (whitespace-separated bit flags / lists). */
|
||||
isList: boolean;
|
||||
allowsDefine: boolean;
|
||||
isBoolean: boolean;
|
||||
base: string | null;
|
||||
@@ -39,6 +41,7 @@ export interface SimpleTypeInfo {
|
||||
refType: string | null;
|
||||
isRef: boolean;
|
||||
enumValues: string[];
|
||||
isList: boolean;
|
||||
allowsDefine: boolean;
|
||||
doc: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user