Skip to content

Commit 9937be0

Browse files
authored
Keyvalue file auto detection (#100)
2 parents 4e85e5a + cc6cd91 commit 9937be0

7 files changed

Lines changed: 220 additions & 31 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@
5252
"onLanguage:vpc",
5353
"onLanguage:cfg",
5454
"onLanguage:rad",
55-
"onLanguage:soundscript"
55+
"onLanguage:soundscript",
56+
"onLanguage:plaintext"
5657
],
5758
"contributes": {
5859
"snippets": [
@@ -192,7 +193,7 @@
192193
"Source Soundscript"
193194
],
194195
"filenamePatterns": [
195-
"game_sounds_*.txt",
196+
"game_sounds_example.txt",
196197
"npc_sounds_*.txt",
197198
"level_sounds_*.txt"
198199
]
@@ -314,6 +315,31 @@
314315
"configuration": {
315316
"title": "Source Engine",
316317
"properties": {
318+
"sourceEngine.kvAutoDetect.enabled": {
319+
"type": "boolean",
320+
"default": false,
321+
"description": "Enable automatic detection of KeyValues files."
322+
},
323+
"sourceEngine.kvAutoDetect.onlyInSourceEngineWorkspaces": {
324+
"type": "boolean",
325+
"default": true,
326+
"description": "Enable automatic detection of KeyValues files only in workspaces."
327+
},
328+
"sourceEngine.sourceEngineWorkspaces": {
329+
"type": "array",
330+
"default": [
331+
"C:\\Program Files (x86)\\Steam\\SteamApps\\common\\Portal 2",
332+
"C:\\Program Files (x86)\\Steam\\SteamApps\\common\\Half Life 2",
333+
"C:\\Program Files (x86)\\Steam\\SteamApps\\common\\Counter Strike Global Offensive",
334+
"C:\\Program Files (x86)\\Steam\\SteamApps\\common\\Team Fortress 2",
335+
"C:\\Program Files (x86)\\Steam\\SteamApps\\common\\Left 4 Dead 2",
336+
"C:\\Program Files (x86)\\Steam\\SteamApps\\common\\Alien Swarm"
337+
],
338+
"description": "List of Source Engine workspaces. Some features are only enabled for files which are located in these workspaces.",
339+
"items": {
340+
"type": "string"
341+
}
342+
},
317343
"sourceEngine.performance.log": {
318344
"type": "boolean",
319345
"default": false,

src/KvFileDetection.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import path from "path";
2+
import * as vscode from "vscode";
3+
import * as main from "./main";
4+
import * as shared from "./language/Shared";
5+
6+
function isAutoDetectEnabled(): boolean {
7+
return main.config.get<boolean>("kvAutoDetect.enabled", false);
8+
}
9+
10+
function isAutoDetectEnabledOnlyInWorkspaces(): boolean {
11+
return main.config.get<boolean>("kvAutoDetect.onlyInSourceEngineWorkspaces", true);
12+
}
13+
14+
function getSourceEngineWorkspaces(): string[] {
15+
return main.config.get<string[]>("sourceEngineWorkspaces", []);
16+
}
17+
18+
19+
type DetectableLanguageId = "keyvalue3" | "soundscript" | "captions";
20+
interface CommonFileName {
21+
regex: RegExp;
22+
languageId: DetectableLanguageId;
23+
}
24+
25+
const commonKvFileNames: CommonFileName[] = [
26+
{ regex: /gameinfo\.txt/i, languageId: "keyvalue3" },
27+
{ regex: /subtitles_.+\.txt/i, languageId: "captions" },
28+
{ regex: /captions_.+\.txt/i, languageId: "captions" },
29+
{ regex: /basemodui_.+\.txt/i, languageId: "keyvalue3" },
30+
{ regex: /instructor_lessons\.txt/i, languageId: "keyvalue3" },
31+
{ regex: /instructor_textures\.txt/i, languageId: "keyvalue3" },
32+
{ regex: /game_sounds_.+\.txt/i, languageId: "soundscript" },
33+
{ regex: /npc_sounds_.+\.txt/i, languageId: "soundscript" },
34+
{ regex: /level_sounds_.+\.txt/i, languageId: "soundscript" },
35+
{ regex: /soundscapes_.+\.txt/i, languageId: "soundscript" },
36+
{ regex: /surfaceproperties_.+\.txt/i, languageId: "keyvalue3" },
37+
{ regex: /weapon_.+\.txt/i, languageId: "keyvalue3" },
38+
{ regex: /vgui_screens\.txt/i, languageId: "keyvalue3" },
39+
{ regex: /credits\.txt/i, languageId: "keyvalue3" }
40+
];
41+
42+
function getCommonFileNameMatch(fileName: string): CommonFileName | undefined {
43+
return commonKvFileNames.find((c) => c.regex.test(fileName));
44+
}
45+
46+
const globalStateAutoDetectRecommended = "kvAutoDetect.recommended";
47+
function recommendAutoDetection(context: vscode.ExtensionContext): void {
48+
49+
const alreadyRecommended = context.globalState.get(globalStateAutoDetectRecommended, false);
50+
if(alreadyRecommended) return;
51+
52+
vscode.window.showInformationMessage("Source Engine keyvalue files can be auto detected. Do you want to enable this feature?", "Yes", "No")
53+
.then((value: string | undefined) => {
54+
if(value === "Yes") {
55+
main.config.update("kvAutoDetect.enabled", true, true);
56+
}
57+
58+
context.globalState.update(globalStateAutoDetectRecommended, true);
59+
});
60+
}
61+
62+
export function init(context: vscode.ExtensionContext): void {
63+
const onChange = vscode.window.onDidChangeActiveTextEditor((editor: vscode.TextEditor | undefined): void => {
64+
main.debugOutput.appendLine(`Active editor changed to ${editor?.document.uri.fsPath}`);
65+
if(editor === undefined) return;
66+
detectKeyvalueFile(editor, context);
67+
});
68+
context.subscriptions.push(onChange);
69+
}
70+
71+
function detectKeyvalueFile(editor: vscode.TextEditor, context: vscode.ExtensionContext): void {
72+
73+
if(!isAutoDetectEnabled()) {
74+
main.debugOutput.appendLine("Auto detect is disabled. Not detecting kv file.");
75+
76+
recommendAutoDetection(context);
77+
return;
78+
}
79+
80+
main.debugOutput.appendLine("File has language id: " + editor.document.languageId);
81+
if(editor.document.languageId === "plaintext") {
82+
main.debugOutput.appendLine(`Potential kv file opened (${editor.document.uri.fsPath})`);
83+
84+
const langId = getPotentialKvFileLanguageId(editor.document);
85+
if(langId === undefined) {
86+
main.debugOutput.appendLine(`Not changing document language of (${editor.document.uri.fsPath})`);
87+
return;
88+
}
89+
90+
main.debugOutput.appendLine(`Changing document language of (${editor.document.uri.fsPath}) to keyvalue3`);
91+
vscode.languages.setTextDocumentLanguage(editor.document, langId);
92+
}
93+
}
94+
95+
function getPotentialKvFileLanguageId(document: vscode.TextDocument): DetectableLanguageId | undefined {
96+
if(isAutoDetectEnabledOnlyInWorkspaces()) {
97+
98+
if(!isDocumentInSourceEngineWorkspace(document)) {
99+
main.debugOutput.appendLine(`Auto detect is enabled only in source engine workspaces, but the document (${document.uri.fsPath}) is not in a source engine workspace.`);
100+
return undefined;
101+
}
102+
103+
}
104+
105+
const documentFileName = path.basename(document.uri.fsPath);
106+
107+
main.debugOutput.appendLine(`Auto detect is enabled. Checking if filename (${documentFileName}) is in list of common kv file names.`);
108+
const match = getCommonFileNameMatch(documentFileName);
109+
if(match == undefined) {
110+
return undefined;
111+
}
112+
113+
main.debugOutput.appendLine(`! File (${document.uri.fsPath}) has a common file name and is associated with LanguageId: ${match.languageId}`);
114+
return match.languageId;
115+
}
116+
117+
function isDocumentInSourceEngineWorkspace(document: vscode.TextDocument): boolean {
118+
let fileUri: string = document.uri.fsPath;
119+
let sourceEngineWorkspaces: string[] = getSourceEngineWorkspaces();
120+
121+
// Make paths lower case on windows
122+
if(process.platform === "win32") {
123+
fileUri = fileUri.toLowerCase();
124+
sourceEngineWorkspaces = sourceEngineWorkspaces.map((workspace) => workspace.toLowerCase());
125+
}
126+
127+
main.debugOutput.appendLine(`Checking if file (${fileUri}) is in any source engine workspace.`);
128+
129+
for(const workspace of sourceEngineWorkspaces) {
130+
main.debugOutput.appendLine(`- Checking if file (${fileUri}) is in workspace (${workspace})`);
131+
if(fileUri.startsWith(workspace)) {
132+
main.debugOutput.appendLine(`! File (${fileUri}) is in workspace (${workspace})`);
133+
return true;
134+
}
135+
}
136+
137+
main.debugOutput.appendLine(`! File (${fileUri}) is not in any source engine workspace.`);
138+
139+
return false;
140+
}
141+

src/language/LangKv.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,18 @@
55
// This is NOT a base for other formats!
66
// ==========================================================================
77

8+
import * as shared from "./Shared";
89
import * as vscode from "vscode";
910
import { KvTokensProviderBase } from "./KvTokensProviderBase";
1011
//import { KvDocumentFormatter } from "./KvFormatter";
1112
import { KvSemanticProcessor, KvSemanticProcessorParams } from "./KvSemanticProcessor";
1213
import KvDocument from "./KvDocument";
1314
import { matrixRegExp } from "@sourcelib/vmt";
1415

15-
export const filterKvSaved: vscode.DocumentFilter = {
16-
language: "keyvalue3",
17-
scheme: "file"
18-
};
19-
export const filterKvUnsaved: vscode.DocumentFilter = {
20-
language: "keyvalue3",
21-
scheme: "untitled"
22-
};
23-
export const filterSoundscriptSaved: vscode.DocumentFilter = {
24-
language: "soundscript",
25-
scheme: "file"
26-
};
27-
export const filterSoundscriptUnsaved: vscode.DocumentFilter = {
28-
language: "soundscript",
29-
scheme: "untitled"
30-
};
31-
export const selectorAll: ReadonlyArray<vscode.DocumentFilter> = [ filterKvSaved, filterKvUnsaved, filterSoundscriptSaved, filterSoundscriptUnsaved ];
16+
export const selectorAll: ReadonlyArray<vscode.DocumentFilter> = [ shared.filterKvSaved,
17+
shared.filterKvUnsaved,
18+
shared.filterSoundscriptSaved,
19+
shared.filterSoundscriptUnsaved ];
3220

3321
export function init(context: vscode.ExtensionContext): void {
3422
const kvTokenProvider = new KeyvalueSemanticTokensProvider();

src/language/LangVmt.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Implementations of language utility providers for the vmt language.
44
// ==========================================================================
55

6+
import * as shared from "./Shared";
67
import vscode from "vscode";
78
import KvDocument from "./KvDocument";
89
//import { KvDocumentFormatter } from "./KvFormatter";
@@ -13,15 +14,8 @@ import { ShaderParamHoverProvider } from "./ShaderParamHoverProvider";
1314
import { ShaderParamColorsProvider } from "./ShaderParamColorsProvider";
1415
import { VmtSemanticTokenProvider } from "./VmtSemanticTokenProvider";
1516

16-
export const filterVmtSaved: vscode.DocumentFilter = {
17-
language: "vmt",
18-
scheme: "file"
19-
};
20-
export const filterVmtUnsaved: vscode.DocumentFilter = {
21-
language: "vmt",
22-
scheme: "untitled"
23-
};
24-
export const selectorAll: ReadonlyArray<vscode.DocumentFilter> = [filterVmtSaved, filterVmtUnsaved];
17+
18+
export const selectorAll: ReadonlyArray<vscode.DocumentFilter> = [shared.filterVmtSaved, shared.filterVmtUnsaved];
2519

2620
export let shaderParams: ShaderParam[];
2721
export let internalTextures: string[];

src/language/Shared.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { DocumentFilter } from "vscode";
2+
3+
export const languageIdKeyvalue = "keyvalue3";
4+
export const languageIdVmt = "vmt";
5+
export const languageIdSoundscript = "soundscript";
6+
export const languageIdCaptions = "captions";
7+
8+
export function isAnyExtensionLanguageId(id: string): boolean {
9+
return id === languageIdKeyvalue || id === languageIdVmt || id === languageIdSoundscript || id === languageIdCaptions;
10+
}
11+
12+
export const filterKvSaved: DocumentFilter = {
13+
language: languageIdKeyvalue,
14+
scheme: "file"
15+
};
16+
export const filterKvUnsaved: DocumentFilter = {
17+
language: languageIdKeyvalue,
18+
scheme: "untitled"
19+
};
20+
export const filterSoundscriptSaved: DocumentFilter = {
21+
language: languageIdSoundscript,
22+
scheme: "file"
23+
};
24+
export const filterSoundscriptUnsaved: DocumentFilter = {
25+
language: languageIdSoundscript,
26+
scheme: "untitled"
27+
};
28+
export const filterVmtSaved: DocumentFilter = {
29+
language: languageIdVmt,
30+
scheme: "file"
31+
};
32+
export const filterVmtUnsaved: DocumentFilter = {
33+
language: languageIdVmt,
34+
scheme: "untitled"
35+
};

src/main.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,26 @@ import * as keyvalue from "./language/LangKv";
1010
import * as captionsCompile from "./compiler/captions-compile";
1111
import * as modelCompile from "./compiler/captions-compile";
1212
import * as performance from "./compiler/model-compile";
13+
import * as kvDetect from "./KvFileDetection";
1314

1415
import * as packageJson from "../package.json";
1516

1617
export let output: vscode.OutputChannel;
18+
export let debugOutput: vscode.OutputChannel;
1719
export let config: vscode.WorkspaceConfiguration;
1820

1921
export function deactivate(): void {}
2022
export function activate(context: vscode.ExtensionContext): void {
2123

2224
output = vscode.window.createOutputChannel("Source Engine Support");
23-
context.subscriptions.push(output);
25+
debugOutput = vscode.window.createOutputChannel("Source Engine Support Debug");
26+
context.subscriptions.push(output, debugOutput);
2427

2528
updateConfig();
2629
const configChangeEvent = vscode.workspace.onDidChangeConfiguration(updateConfig);
2730
context.subscriptions.push(configChangeEvent);
31+
32+
kvDetect.init(context);
2833

2934
keyvalue.init(context);
3035
vmt.init(context);

0 commit comments

Comments
 (0)