Skip to content

Commit 16d6878

Browse files
committed
Refactor project structure, implement conditional support for kv and fix tokenization bugs
1 parent 368de15 commit 16d6878

28 files changed

+1177
-955
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"lint": "eslint ./src",
4343
"deploy": "vsce publish"
4444
},
45-
"main": "./out/main.js",
45+
"main": "./out/extension/main.js",
4646
"activationEvents": [
4747
"onLanguage:keyvalue3",
4848
"onLanguage:vmt",

src/extension/Kv.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Range } from "vscode";
2+
3+
export class KvPair {
4+
5+
public key: KvPiece;
6+
public values: KvPiece[];
7+
8+
constructor(key: KvPiece, values: KvPiece[]) {
9+
this.key = key;
10+
this.values = values;
11+
}
12+
13+
public get value(): KvPiece {
14+
return this.values[0];
15+
}
16+
17+
}
18+
19+
export class KvPiece {
20+
public content: string;
21+
public range: Range;
22+
23+
constructor(content: string, range: Range) {
24+
this.content = content;
25+
this.range = range;
26+
}
27+
}

src/compiler/captions-compile.ts renamed to src/extension/compiler/captions-compile.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@ import { compileSomething } from "./compiler-base";
88

99
let ccChannel: OutputChannel;
1010

11-
export function init(context: ExtensionContext): void {
12-
const ccCommand = commands.registerTextEditorCommand("captions.compile", compileCaptions);
13-
context.subscriptions.push(ccCommand);
11+
export async function init(context: ExtensionContext): Promise<void> {
12+
const commandList = await commands.getCommands(true);
13+
const exists = commandList.some(c => c === "captions.compile");
14+
if(!exists) {
15+
const ccCommand = commands.registerTextEditorCommand("captions.compile", compileCaptions);
16+
context.subscriptions.push(ccCommand);
17+
}
1418
}
1519

1620
async function compileCaptions(editor: TextEditor): Promise<void> {

src/compiler/compiler-base.ts renamed to src/extension/compiler/compiler-base.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,40 @@
1-
import { FileType, OutputChannel, ProgressLocation, TextEditor, Uri, window, workspace } from "vscode";
2-
import { isWhitespace } from "../kv-core/kv-string-util";
3-
import { config } from "../main";
1+
import vscode from "vscode";
2+
import { isWhitespace } from "../../kv-core/kv-string-util";
3+
import * as main from "../main";
44
import * as fs from "fs";
55
import { execFile } from "child_process";
66

77
export interface CompileSettings {
8-
editor: TextEditor;
9-
channel: OutputChannel | undefined;
8+
editor: vscode.TextEditor;
9+
channel: vscode.OutputChannel | undefined;
1010
compilerName: string;
1111
configPrefix: string;
1212
}
1313

1414
export async function compileSomething(settings: CompileSettings): Promise<void> {
1515

16-
const channel = settings.channel ?? window.createOutputChannel(settings.compilerName);
16+
const channel = settings.channel ?? vscode.window.createOutputChannel(settings.compilerName);
1717

18-
const exePath: string = config.get(`${settings.configPrefix}.executablePath`) ?? "";
19-
const shouldOpenOutputWindow: boolean = config.get(`${settings.configPrefix}.openOutputWindow`) ?? false;
20-
const params: string[] = config.get(`${settings.configPrefix}.additionalParameters`) ?? [];
18+
const exePath: string = main.config.get(`${settings.configPrefix}.executablePath`) ?? "";
19+
const shouldOpenOutputWindow: boolean = main.config.get(`${settings.configPrefix}.openOutputWindow`) ?? false;
20+
const params: string[] = main.config.get(`${settings.configPrefix}.additionalParameters`) ?? [];
2121

2222
const fileUri = settings.editor.document.uri;
23-
const fileStat = await workspace.fs.stat(fileUri);
24-
if(fileStat.type !== FileType.File) {
25-
window.showErrorMessage("The current document is not a file. Cannot compile. Honestly, I have no idea how this is possible, so if you can into this, please message me.");
23+
const fileStat = await vscode.workspace.fs.stat(fileUri);
24+
if(fileStat.type !== vscode.FileType.File) {
25+
vscode.window.showErrorMessage("The current document is not a file. Cannot compile. Honestly, I have no idea how this is possible, so if you can into this, please message me.");
2626
return;
2727
}
2828
const filePath = fileUri.fsPath;
29-
const workDir: string = config.get(`${settings.configPrefix}.workingDirectory`) ?? Uri.joinPath(fileUri, "..").fsPath;
29+
const workDir: string = main.config.get(`${settings.configPrefix}.workingDirectory`) ?? vscode.Uri.joinPath(fileUri, "..").fsPath;
3030

3131
if( exePath == null || isWhitespace(exePath) ) {
32-
window.showErrorMessage(`${settings.compilerName} path is empty. Please configure!`);
32+
vscode.window.showErrorMessage(`${settings.compilerName} path is empty. Please configure!`);
3333
return;
3434
}
3535

3636
if( !fs.existsSync(exePath) ) {
37-
window.showErrorMessage(`${settings.compilerName} executeable at '${exePath}' does not exist.`);
37+
vscode.window.showErrorMessage(`${settings.compilerName} executeable at '${exePath}' does not exist.`);
3838
return;
3939
}
4040

@@ -59,8 +59,8 @@ export async function compileSomething(settings: CompileSettings): Promise<void>
5959
if(stderr) channel.appendLine("! " + stderr);
6060
});
6161

62-
window.withProgress({
63-
location: ProgressLocation.Notification,
62+
vscode.window.withProgress({
63+
location: vscode.ProgressLocation.Notification,
6464
title: settings.compilerName,
6565
cancellable: true
6666
}, (progress, token) => {

src/compiler/model-compile.ts renamed to src/extension/compiler/model-compile.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,19 @@
33
// Adds commands to compile .qc (model) files.
44
// ==========================================================================
55

6-
import { commands, ExtensionContext, OutputChannel, TextEditor, window } from "vscode";
7-
import { config } from "../main";
6+
import vscode from "vscode";
87
import { compileSomething } from "./compiler-base";
98

10-
let qcChannel: OutputChannel;
9+
let qcChannel: vscode.OutputChannel;
1110

12-
export function init(context: ExtensionContext): void {
13-
const ccCommand = commands.registerTextEditorCommand("mdl.compile", compileModel);
11+
export function init(context: vscode.ExtensionContext): void {
12+
const ccCommand = vscode.commands.registerTextEditorCommand("mdl.compile", compileModel);
1413
context.subscriptions.push(ccCommand);
1514
}
1615

17-
async function compileModel(editor: TextEditor): Promise<void> {
16+
async function compileModel(editor: vscode.TextEditor): Promise<void> {
1817
if(qcChannel == null) {
19-
qcChannel = window.createOutputChannel("Model Compiler");
18+
qcChannel = vscode.window.createOutputChannel("Model Compiler");
2019
}
2120

2221
await compileSomething({
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import vscode from "vscode";
2+
import KvDocument from "./KvDocument";
3+
import { populateColorTagMatches } from "../../kv-core/kv-caption-tag-matches";
4+
5+
6+
export class CaptionsColorsProvider implements vscode.DocumentColorProvider {
7+
8+
provideDocumentColors(document: vscode.TextDocument, cancellationToken: vscode.CancellationToken): vscode.ColorInformation[] {
9+
const lines = document.lineCount;
10+
11+
const kvDoc = KvDocument.from(document);
12+
if (kvDoc == null)
13+
return [];
14+
15+
const colorInfos: vscode.ColorInformation[] = [];
16+
17+
for (let i = 0; i < lines; i++) {
18+
if (cancellationToken.isCancellationRequested)
19+
break;
20+
21+
// Get a line that isn't empty
22+
const kv = kvDoc.getKeyValueAt(i);
23+
if (kv == null)
24+
continue;
25+
26+
const clrInfo = populateColorTagMatches(kv.value.content);
27+
clrInfo.forEach((clr) => {
28+
const colorInfo = new vscode.ColorInformation(kv.value.range.with(kv.value.range.start.translate(0, clr.start), kv.value.range.start.translate(0, clr.end)),
29+
new vscode.Color(clr.color.r / 255, clr.color.g / 255, clr.color.b / 255, 1.0));
30+
colorInfos.push(colorInfo);
31+
});
32+
}
33+
34+
return colorInfos;
35+
}
36+
37+
provideColorPresentations(color: vscode.Color, context: { document: vscode.TextDocument; range: vscode.Range; }, cancellationToken: vscode.CancellationToken): vscode.ColorPresentation[] {
38+
return [];
39+
}
40+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import vscode from "vscode";
2+
import KvDocument from "./KvDocument";
3+
import { KvTokensProviderBase } from "./KvTokensProviderBase";
4+
import { KvSemanticProcessor, KvSemanticProcessorParams } from "./KvSemanticProcessor";
5+
6+
7+
export class CaptionsSemanticTokenProvider extends KvTokensProviderBase {
8+
9+
protected keyProcessors: KvSemanticProcessor[] = [
10+
{ regex: /.*/, processor: this.processKey }
11+
];
12+
protected valueProcessors: KvSemanticProcessor[] = [
13+
{ regex: /.*/, processor: this.processValue }
14+
];
15+
16+
constructor() {
17+
super(KvDocument.tokenLegend, vscode.languages.createDiagnosticCollection("captions"));
18+
}
19+
20+
processKey(params: KvSemanticProcessorParams): boolean {
21+
params.tokensBuilder.push(params.wholeRange, "parameter");
22+
return true;
23+
}
24+
25+
processValue(params: KvSemanticProcessorParams): boolean {
26+
27+
if (params.scope === ".lang.tokens") {
28+
return true; // Don't add a semantic token to lang values and let tmLanguage handle it.
29+
}
30+
params.tokensBuilder.push(params.wholeRange, "string");
31+
return true;
32+
}
33+
34+
}

src/extension/language/KvDocument.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import * as vscode from "vscode";
2+
import { KvPair, KvPiece } from "../Kv";
3+
import { Token, Tokenizer, TokenType } from "../../kv-core/kv-tokenizer";
4+
import { KvTokensProviderBase } from "./KvTokensProviderBase";
5+
6+
export default class KvDocument {
7+
8+
protected _document: vscode.TextDocument;
9+
protected _tokens: Token[];
10+
11+
public get document(): vscode.TextDocument {
12+
return this._document;
13+
}
14+
15+
public get tokens(): Token[] {
16+
return this._tokens;
17+
}
18+
19+
private static tokenizer: Tokenizer = new Tokenizer();
20+
21+
public static tokenize(document: vscode.TextDocument): Token[] {
22+
const text = document.getText();
23+
this.tokenizer.tokenizeFile(text);
24+
const tokens = this.tokenizer.tokens;
25+
26+
return tokens;
27+
}
28+
29+
public static from(document: vscode.TextDocument): KvDocument {
30+
return new KvDocument(document, this.tokenize(document));
31+
}
32+
33+
public static tokenLegend = new vscode.SemanticTokensLegend([
34+
"struct",
35+
"comment",
36+
"variable",
37+
"string",
38+
"number",
39+
"operator",
40+
"macro",
41+
"boolean",
42+
"keyword",
43+
"parameter"
44+
], [
45+
"declaration",
46+
"readonly"
47+
]);
48+
49+
private constructor(document: vscode.TextDocument, tks: Token[]) {
50+
this._document = document;
51+
this._tokens = tks;
52+
}
53+
54+
public getKeyValueAt(lineNumber: number): KvPair | null {
55+
56+
const line = this._document.lineAt(lineNumber);
57+
if (line.isEmptyOrWhitespace)
58+
return null;
59+
const tokens = this.findTokensOnLine(lineNumber);
60+
61+
// Normal old keyvalue
62+
if (tokens.length == 0)
63+
return null;
64+
65+
let keyPiece: KvPiece | null = null;
66+
const valuePieces: KvPiece[] = [];
67+
for (const token of tokens) {
68+
switch (token.type) {
69+
case TokenType.Key:
70+
keyPiece = this.getUnquotedToken(token); break;
71+
case TokenType.Value:
72+
valuePieces.push(this.getUnquotedToken(token)); break;
73+
}
74+
}
75+
76+
if (keyPiece == null)
77+
return null;
78+
79+
return new KvPair(keyPiece, valuePieces);
80+
}
81+
82+
public getAllValueTokens(): Token[] {
83+
return this._tokens.filter(t => t.type === TokenType.Value);
84+
}
85+
86+
public getAllTokens(): Token[] {
87+
return this._tokens;
88+
}
89+
90+
public findTokensOnLine(line: number): Token[] {
91+
return this._tokens.filter(t => t.line == line);
92+
}
93+
94+
private getTokenRange(token: Token): vscode.Range {
95+
return new vscode.Range(this.document.positionAt(token.start), this.document.positionAt(token.end));
96+
}
97+
98+
private getUnquotedToken(token: Token): KvPiece {
99+
const range = this.getTokenRange(token);
100+
return KvTokensProviderBase.unquoteToken(token, range);
101+
}
102+
103+
}

src/extension/language/KvFormatter.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import vscode from "vscode";
2+
import { formatTokens } from "../../kv-core/kv-formatter";
3+
import KvDocument from "./KvDocument";
4+
5+
6+
7+
export class KvDocumentFormatter implements vscode.DocumentFormattingEditProvider {
8+
9+
10+
11+
protected doPutBracesOnNewline(): boolean {
12+
const config = vscode.workspace.getConfiguration("sourceEngine");
13+
return config.get("keyvalueBracesOnNewline") ?? false;
14+
}
15+
16+
provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): vscode.TextEdit[] {
17+
18+
const tokens = KvDocument.tokenize(document);
19+
if (tokens == null)
20+
return [];
21+
22+
const startPos = document.positionAt(0);
23+
24+
// We just delete and reconstruct the entire file. Much easier.
25+
const edits: vscode.TextEdit[] = [
26+
new vscode.TextEdit(new vscode.Range(startPos, document.positionAt(document.getText().length)), "")
27+
];
28+
29+
edits.push(new vscode.TextEdit(new vscode.Range(startPos, startPos), formatTokens(tokens, this.doPutBracesOnNewline())));
30+
31+
return edits;
32+
33+
}
34+
35+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { KvPiece } from "../Kv";
2+
import { Range, SemanticTokensBuilder } from "vscode";
3+
import KvDocument from "./KvDocument";
4+
5+
export type KvSemanticProcessorFunction = (options: KvSemanticProcessorParams) => boolean;
6+
7+
export interface KvSemanticProcessorParams {
8+
kvPiece: KvPiece;
9+
wholeRange: Range;
10+
tokensBuilder: SemanticTokensBuilder;
11+
captures: RegExpMatchArray;
12+
kvDocument: KvDocument;
13+
scope: string;
14+
}
15+
16+
export class KvSemanticProcessor {
17+
public processor: KvSemanticProcessorFunction;
18+
public regex = /.+/;
19+
20+
constructor(processor: KvSemanticProcessorFunction, regex: RegExp) {
21+
this.processor = processor;
22+
this.regex = regex;
23+
}
24+
}

0 commit comments

Comments
 (0)