mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
feat(editor): add syntax highlighting for go.mod and go.work files (#1695)
go.mod and go.work opened as plain text: their language definitions had no ui-mode, so no CodeMirror language loaded. Add a GoMod mode (a small stream parser): directives as keywords, module paths as strings, versions as numbers, // comments; shared by go.mod and go.work. Co-authored-by: elfadl <[email protected]>
This commit is contained in:
1 parent
1af1744290
commit
14a31fe8a1
7 files changed
+196
-1
No files matched your search
@@ -0,0 +1,73 @@
|
||||
// @vitest-environment node
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { goModParser, initialGoModState, type GoModState } from "./gomod";
|
||||
import { StringStream } from "@codemirror/language";
|
||||
|
||||
function tokenize(line: string, state: GoModState) {
|
||||
const stream = new StringStream(line, 4, 1);
|
||||
const tokens: { text: string; token: string | null }[] = [];
|
||||
while (!stream.eol()) {
|
||||
const start = stream.pos;
|
||||
const kind = goModParser.token(stream, state);
|
||||
if (stream.pos > start && kind !== null) {
|
||||
tokens.push({ text: stream.string.slice(start, stream.pos), token: kind });
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
describe("goModParser", () => {
|
||||
it("highlights module declaration and go version", () => {
|
||||
const state = initialGoModState();
|
||||
|
||||
const line1 = tokenize("module onpay-aplikasi-api", state);
|
||||
expect(line1).toEqual([
|
||||
{ text: "module", token: "keyword" },
|
||||
{ text: "onpay-aplikasi-api", token: "string" },
|
||||
]);
|
||||
|
||||
const line2 = tokenize("go 1.27.0", state);
|
||||
expect(line2).toEqual([
|
||||
{ text: "go", token: "keyword" },
|
||||
{ text: "1.27.0", token: "number" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("highlights require blocks with module paths and versions", () => {
|
||||
const state = initialGoModState();
|
||||
|
||||
const line1 = tokenize("require (", state);
|
||||
expect(line1).toEqual([
|
||||
{ text: "require", token: "keyword" },
|
||||
{ text: "(", token: "paren" },
|
||||
]);
|
||||
|
||||
const line2 = tokenize("\tcloud.google.com/go/auth v0.20.0", state);
|
||||
expect(line2).toEqual([
|
||||
{ text: "cloud.google.com/go/auth", token: "string" },
|
||||
{ text: "v0.20.0", token: "number" },
|
||||
]);
|
||||
|
||||
const line3 = tokenize("\tgithub.com/shopspring/decimal v1.4.0 // indirect", state);
|
||||
expect(line3).toEqual([
|
||||
{ text: "github.com/shopspring/decimal", token: "string" },
|
||||
{ text: "v1.4.0", token: "number" },
|
||||
{ text: "// indirect", token: "comment" },
|
||||
]);
|
||||
|
||||
const line4 = tokenize(")", state);
|
||||
expect(line4).toEqual([{ text: ")", token: "paren" }]);
|
||||
});
|
||||
|
||||
it("highlights replace directive with operator", () => {
|
||||
const state = initialGoModState();
|
||||
|
||||
const line = tokenize("replace example.com/foo => ../bar", state);
|
||||
expect(line).toEqual([
|
||||
{ text: "replace", token: "keyword" },
|
||||
{ text: "example.com/foo", token: "string" },
|
||||
{ text: "=>", token: "operator" },
|
||||
{ text: "../bar", token: "string" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import { StreamLanguage, type StreamParser } from "@codemirror/language";
|
||||
|
||||
const KEYWORDS = new Set(["module", "go", "toolchain", "require", "replace", "exclude", "retract", "use"]);
|
||||
|
||||
export interface GoModState {
|
||||
currentKeyword: string | null;
|
||||
inBlock: boolean;
|
||||
expectVersion: boolean;
|
||||
}
|
||||
|
||||
export function initialGoModState(): GoModState {
|
||||
return {
|
||||
currentKeyword: null,
|
||||
inBlock: false,
|
||||
expectVersion: false,
|
||||
};
|
||||
}
|
||||
|
||||
export const goModParser: StreamParser<GoModState> = {
|
||||
name: "gomod",
|
||||
startState: () => initialGoModState(),
|
||||
token(stream, state) {
|
||||
if (stream.eatSpace()) return null;
|
||||
|
||||
if (stream.sol()) {
|
||||
if (!state.inBlock) {
|
||||
state.currentKeyword = null;
|
||||
}
|
||||
state.expectVersion = false;
|
||||
}
|
||||
|
||||
// Comment
|
||||
if (stream.match(/^\/\/.*/)) {
|
||||
return "comment";
|
||||
}
|
||||
|
||||
// Punctuation & operators
|
||||
if (stream.match(/^=>/)) {
|
||||
state.expectVersion = false;
|
||||
return "operator";
|
||||
}
|
||||
if (stream.eat("(")) {
|
||||
state.inBlock = true;
|
||||
return "paren";
|
||||
}
|
||||
if (stream.eat(")")) {
|
||||
state.inBlock = false;
|
||||
state.currentKeyword = null;
|
||||
return "paren";
|
||||
}
|
||||
|
||||
// String literals
|
||||
if (stream.match(/^"(?:[^"\\]|\\.)*"/)) {
|
||||
return "string";
|
||||
}
|
||||
if (stream.match(/^`[^`]*`/)) {
|
||||
return "string";
|
||||
}
|
||||
|
||||
// Check words/tokens
|
||||
const wordMatch = stream.match(/^[^\s()]+/) as RegExpMatchArray | null;
|
||||
if (!wordMatch) {
|
||||
stream.next();
|
||||
return null;
|
||||
}
|
||||
|
||||
const token = wordMatch[0] ?? "";
|
||||
|
||||
// Keyword at start of statement (not expecting a version)
|
||||
if (!state.expectVersion && KEYWORDS.has(token)) {
|
||||
state.currentKeyword = token;
|
||||
return "keyword";
|
||||
}
|
||||
|
||||
// Semantic versions: v1.2.3, 1.22, 1.22.0, v0.0.0-2023..., v4.4.0+incompatible
|
||||
const isVersion = /^v?[0-9]+(\.[0-9]+)*([+-][a-zA-Z0-9_.-]+)*$/.test(token) || /^\[v?[0-9]+.*\]$/.test(token);
|
||||
|
||||
if (isVersion) {
|
||||
state.expectVersion = false;
|
||||
return "number";
|
||||
}
|
||||
|
||||
// In require/replace/use block or following module/require keyword:
|
||||
// The first token is the module path/package name (string in GoLand: green)
|
||||
if (state.currentKeyword) {
|
||||
state.expectVersion = true;
|
||||
return "string";
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
languageData: {
|
||||
commentTokens: { line: "//" },
|
||||
indentOnInput: /^\s*\)\s*$/,
|
||||
},
|
||||
};
|
||||
|
||||
export const goModLanguage = StreamLanguage.define(goModParser);
|
||||
@@ -151,4 +151,18 @@ describe("language modes", () => {
|
||||
expect(derefs, mode).toEqual(["?.", ".", "?."]);
|
||||
}
|
||||
});
|
||||
|
||||
it("recognizes go.mod and go.work files by mode and filename", async () => {
|
||||
for (const [mode, path] of [
|
||||
["GoMod", "/project/go.mod"],
|
||||
[null, "/project/go.mod"],
|
||||
["GoMod", "/project/go.work"],
|
||||
[null, "/project/go.work"],
|
||||
] as const) {
|
||||
const desc = findLanguageDescription(mode, path);
|
||||
expect(desc?.name).toBe("GoMod");
|
||||
const support = await loadSupport(desc!);
|
||||
expect(support?.language).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -141,6 +141,13 @@ const OWN_MODES = [
|
||||
);
|
||||
},
|
||||
}),
|
||||
LanguageDescription.of({
|
||||
name: "GoMod",
|
||||
alias: ["gomod", "gowork"],
|
||||
extensions: ["mod", "work"],
|
||||
filename: /^(?:go\.mod|go\.work)$/,
|
||||
load: async () => new LanguageSupport((await import("./gomod")).goModLanguage),
|
||||
}),
|
||||
];
|
||||
|
||||
const BATCH_KEYWORDS = new Set([
|
||||
@@ -224,9 +231,9 @@ export function findLanguageDescription(uiMode: string | null, path: string): La
|
||||
const loose = LanguageDescription.matchLanguageName(BUILTIN_MODES, uiMode, false);
|
||||
if (loose) return OWN_MODES.find((l) => l.name === loose.name) ?? loose;
|
||||
}
|
||||
if (uiMode === null && infos) return null;
|
||||
const own = LanguageDescription.matchFilename(OWN_MODES, path.slice(path.lastIndexOf("/") + 1));
|
||||
if (own) return own;
|
||||
if (uiMode === null && infos) return null;
|
||||
const matched = LanguageDescription.matchFilename(BUILTIN_MODES, path.slice(path.lastIndexOf("/") + 1));
|
||||
return OWN_MODES.find((l) => l.name === matched?.name) ?? matched;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user