Skip to content

Commit 4a169fe

Browse files
authored
Fix infinite tokenizer loop (#341)
* Major code cleanup * Fix infinite loop in tokenizer * Fix rgb color not accepting `.0` float format
1 parent 3745d66 commit 4a169fe

20 files changed

+610
-505
lines changed

.eslintrc.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"require-await": "warn",
2323
"camelcase": "error",
2424
"@typescript-eslint/no-var-requires": "warn",
25-
"no-param-reassign": "warn" // Disable reassign, since this basically means you override the reference from the caller function with a new local version. (It doesn't do what you expect)
25+
"no-param-reassign": "warn", // Disable reassign, since this basically means you override the reference from the caller function with a new local version. (It doesn't do what you expect)
26+
"@typescript-eslint/no-namespace": "off"
2627
}
2728
}

.vscode/launch.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"${workspaceFolder}/examples" // open examples directory
1616
],
1717
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
18-
"preLaunchTask": "npm: watch"
18+
"preLaunchTask": "npm: watch",
19+
"skipFiles": ["<node_internals>/**", "**/extensions/git*/**", "**/node_modules/prettier/**", "**/node/extensionHostProcess.js"]
1920
},
2021
{
2122
"name": "Launch Extension (Release)",
@@ -27,7 +28,8 @@
2728
"${workspaceFolder}/examples" // open examples directory
2829
],
2930
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
30-
"preLaunchTask": "npm: watch-release"
31+
"preLaunchTask": "npm: watch-release",
32+
"skipFiles": ["<node_internals>/**", "**/extensions/git*/**", "**/node_modules/prettier/**"]
3133
},
3234
{
3335
"name": "Extension Tests",
@@ -36,7 +38,8 @@
3638
"runtimeExecutable": "${execPath}",
3739
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test"],
3840
"outFiles": ["${workspaceFolder}/out/test/**/*.js"],
39-
"preLaunchTask": "npm: test-compile"
41+
"preLaunchTask": "npm: test-compile",
42+
"skipFiles": ["<node_internals>/**", "**/extensions/git*/**", "**/node_modules/prettier/**"]
4043
}
4144
]
4245
}

TODO.md

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ Renpy Features List
33
- Support launching the project through VSCode
44

55
- Bugs to fix:
6-
* Fix error on restart
76
* Show color editor in tags (also check https://www.renpy.org/doc/html/color_class.html)
87
* % can be escaped in strings
98
* if line contains unclosed ( [ or { line is continued (see https://www.renpy.org/doc/html/language_basics.html#logical-lines)

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "languague-renpy",
33
"displayName": "Ren'Py Language",
44
"description": "Adds rich support for the Ren'Py programming language to Visual Studio Code.",
5-
"version": "2.3.3",
5+
"version": "2.3.4",
66
"publisher": "LuqueDaniel",
77
"license": "MIT",
88
"homepage": "https://github.com/LuqueDaniel/vscode-language-renpy",

src/color.ts

+74-37
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,44 @@
11
// Color conversion methods for Color provider
2-
import { CancellationToken, Color, ColorInformation, ColorPresentation, DocumentColorProvider, Range, TextDocument, TextEdit } from "vscode";
2+
import { CancellationToken, Color, ColorInformation, ColorPresentation, ProviderResult, Range, TextDocument, TextEdit, languages } from "vscode";
33
import { ValueEqualsSet } from "./utilities/hashset";
4-
import { tokenizeDocument } from "./tokenizer/tokenizer";
4+
import { Tokenizer } from "./tokenizer/tokenizer";
55
import { LiteralTokenType } from "./tokenizer/renpy-tokens";
66
import { TextMateRule, injectCustomTextmateTokens } from "./decorator";
7-
/*import { tokenizeDocument } from "./tokenizer/tokenizer";
8-
import { injectCustomTextmateTokens, TextMateRule } from "./decorator";
9-
import { LiteralTokenType } from "./tokenizer/renpy-tokens";
10-
import { ValueEqualsSet } from "./utilities/hashset";*/
117

12-
export class RenpyColorProvider implements DocumentColorProvider {
13-
public provideDocumentColors(document: TextDocument, token: CancellationToken): Thenable<ColorInformation[]> {
14-
return getColorInformation(document);
15-
}
16-
public provideColorPresentations(color: Color, context: { document: TextDocument; range: Range }, token: CancellationToken): Thenable<ColorPresentation[]> {
17-
return getColorPresentations(color, context.document, context.range);
18-
}
19-
}
8+
export type DocumentColorContext = {
9+
document: TextDocument;
10+
range: Range;
11+
};
12+
13+
export const colorProvider = languages.registerColorProvider("renpy", {
14+
provideDocumentColors(document: TextDocument, token: CancellationToken): ProviderResult<ColorInformation[]> {
15+
if (token.isCancellationRequested) {
16+
return;
17+
}
18+
19+
return new Promise((resolve) => {
20+
resolve(getColorInformation(document));
21+
});
22+
},
23+
24+
provideColorPresentations(color: Color, context: DocumentColorContext, token: CancellationToken): ProviderResult<ColorPresentation[]> {
25+
if (token.isCancellationRequested) {
26+
return;
27+
}
28+
29+
return new Promise((resolve) => {
30+
resolve(getColorPresentations(color, context));
31+
});
32+
},
33+
});
2034

2135
/**
2236
* Finds all colors in the given document and returns their ranges and color
2337
* @param document - the TextDocument to search
24-
* @returns - Thenable<ColorInformation[]> - an array that provides a range and color for each match
38+
* @returns - ColorInformation[] - an array that provides a range and color for each match
2539
*/
26-
export function getColorInformation(document: TextDocument): Thenable<ColorInformation[]> {
27-
injectCustomColorStyles(document);
40+
export async function getColorInformation(document: TextDocument) {
41+
await injectCustomColorStyles(document);
2842

2943
// find all colors in the document
3044
const colors: ColorInformation[] = [];
@@ -67,7 +81,7 @@ export function getColorInformation(document: TextDocument): Thenable<ColorInfor
6781
}
6882
}
6983
}
70-
return Promise.resolve(colors);
84+
return colors;
7185
}
7286

7387
/**
@@ -77,12 +91,12 @@ export function getColorInformation(document: TextDocument): Thenable<ColorInfor
7791
* @param range - The Range of the color match
7892
* @returns - ColorPresentation to replace the color in the document with the new chosen color
7993
*/
80-
export function getColorPresentations(color: Color, document: TextDocument, range: Range): Thenable<ColorPresentation[]> {
94+
export function getColorPresentations(color: Color, context: DocumentColorContext): ColorPresentation[] {
8195
// user hovered/tapped the color block/return the color they picked
8296
const colors: ColorPresentation[] = [];
83-
const line = document.lineAt(range.start.line).text;
84-
const text = line.substring(range.start.character, range.end.character);
85-
const oldRange = new Range(range.start.line, range.start.character, range.start.line, range.start.character + text.length);
97+
const range = context.range;
98+
const text = context.document.getText(range);
99+
const oldRange = new Range(range.start, range.end);
86100

87101
const colR = Math.round(color.red * 255);
88102
const colG = Math.round(color.green * 255);
@@ -112,12 +126,12 @@ export function getColorPresentations(color: Color, document: TextDocument, rang
112126
colors.push(rgbColorPres);
113127
}
114128

115-
return Promise.resolve(colors);
129+
return colors;
116130
}
117131

118-
export function injectCustomColorStyles(document: TextDocument) {
132+
export async function injectCustomColorStyles(document: TextDocument) {
119133
// Disabled until filter is added to the tree class
120-
const documentTokens = tokenizeDocument(document);
134+
const documentTokens = await Tokenizer.tokenizeDocument(document);
121135
// TODO: Should probably make sure this constant is actually part of a tag, but for now this is fine.
122136
const colorTags = documentTokens.filter((x) => x.token?.tokenType === LiteralTokenType.Color);
123137
const colorRules = new ValueEqualsSet<TextMateRule>();
@@ -221,32 +235,55 @@ export function convertHtmlToColor(htmlHex: string): Color | null {
221235
*/
222236
export function convertRenpyColorToColor(renpy: string): Color | null {
223237
try {
224-
const colorTuple = renpy.replace("Color(", "").replace("color", "").replace("=", "").replace(" ", "").replace("(", "[").replace(")", "]");
225-
const result = JSON.parse(colorTuple);
226-
if (result.length === 3) {
227-
return new Color(parseInt(result[0], 16) / 255, parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, 1.0);
228-
} else if (result.length === 4) {
229-
return new Color(parseInt(result[0], 16) / 255, parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255);
238+
const colorTuple = renpy
239+
.replaceAll(" ", "")
240+
.replace(/[Cc]olor=?\(/g, "")
241+
.replace(")", "");
242+
243+
const result = colorTuple.split(",");
244+
if (result.length < 3) {
245+
return null;
230246
}
231-
return null;
247+
248+
const r = parseInt(result[0], 16) / 255;
249+
const g = parseInt(result[1], 16) / 255;
250+
const b = parseInt(result[2], 16) / 255;
251+
const a = result.length === 4 ? parseInt(result[3], 16) / 255 : 1.0;
252+
return new Color(r, g, b, a);
232253
} catch (error) {
233254
return null;
234255
}
235256
}
236257

258+
/**
259+
* Returns a float value based on the given Ren'Py float string value
260+
* @remarks Values starting with a dot (e.g., `.5`) are forced to be parsed as `0.5` due to javascript's `parseFloat` behavior.
261+
* @param value The renpy float value to parse
262+
* @returns The parsed float value
263+
*/
264+
function parseRenpyFloat(value: string): number {
265+
if (value.startsWith(".")) {
266+
return parseFloat("0" + value);
267+
}
268+
return parseFloat(value);
269+
}
270+
237271
/**
238272
* Returns a Color provider object based on the given Ren'Py rgb tuple
239-
* @remarks
240-
* The rgb tuple values should be numeric values between 0.0 and 1.0 (e.g., `rgb=(1.0, 0.0, 0.0)`)
273+
* @remarks The rgb tuple values should be numeric values between 0.0 and 1.0 (e.g., `rgb=(1.0, 0.0, 0.0)`).
274+
* Values starting with a dot (e.g., `.5`) are forced to be parsed as `0.5` due to javascript's `parseFloat` behavior.
241275
* @param renpyColor - Renpy `rgb` color tuple (e.g., `rgb=(r, g, b)`)
242276
* @returns The `Color` provider object
243277
*/
244278
export function convertRgbColorToColor(renpyColor: string): Color | null {
245279
try {
246-
const colorTuple = renpyColor.replace("rgb", "").replace("=", "").replace(" ", "").replace("(", "[").replace(")", "]");
247-
const result = JSON.parse(colorTuple);
280+
const colorTuple = renpyColor
281+
.replaceAll(" ", "")
282+
.replace(/rgb=\(/g, "")
283+
.replace(")", "");
284+
const result = colorTuple.split(",");
248285
if (result.length === 3) {
249-
return new Color(parseFloat(result[0]), parseFloat(result[1]), parseFloat(result[2]), 1.0);
286+
return new Color(parseRenpyFloat(result[0]), parseRenpyFloat(result[1]), parseRenpyFloat(result[2]), 1.0);
250287
}
251288
return null;
252289
} catch (error) {

src/completion.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,30 @@
11
// Completion Provider
2-
import { TextDocument, Position, CompletionContext, CompletionItem, CompletionTriggerKind, CompletionItemKind, workspace } from "vscode";
2+
import { TextDocument, Position, CompletionContext, CompletionItem, CompletionTriggerKind, CompletionItemKind, workspace, languages, CancellationToken, ProviderResult } from "vscode";
33
import { Displayable } from "./displayable";
44
import { getDefinitionFromFile } from "./hover";
55
import { getCurrentContext } from "./navigation";
66
import { NavigationData } from "./navigation-data";
77

8+
export const completionProvider = languages.registerCompletionItemProvider(
9+
"renpy",
10+
{
11+
provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult<CompletionItem[]> {
12+
if (token.isCancellationRequested) {
13+
return;
14+
}
15+
16+
return new Promise((resolve) => {
17+
resolve(getCompletionList(document, position, context));
18+
});
19+
},
20+
},
21+
".",
22+
" ",
23+
"@",
24+
"-",
25+
"("
26+
);
27+
828
/**
929
* Returns an array of auto-complete items related to the keyword at the given document/position
1030
* @param document - The current TextDocument

src/configuration.ts

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { ConfigurationTarget, ExtensionContext, WorkspaceConfiguration, workspace } from "vscode";
2+
3+
export class Configuration {
4+
public static initialize(context: ExtensionContext) {
5+
// hide rpyc files if the setting is enabled
6+
const config = workspace.getConfiguration("renpy");
7+
if (config?.excludeCompiledFilesFromWorkspace) {
8+
this.excludeCompiledFilesConfig();
9+
}
10+
11+
// Listen to configuration changes
12+
context.subscriptions.push(
13+
workspace.onDidChangeConfiguration((e) => {
14+
if (e.affectsConfiguration("renpy.excludeCompiledFilesFromWorkspace")) {
15+
if (workspace.getConfiguration("renpy").get("excludeCompiledFilesFromWorkspace")) {
16+
this.excludeCompiledFilesConfig();
17+
}
18+
}
19+
})
20+
);
21+
}
22+
23+
public static isAutoSaveDisabled(): boolean {
24+
const config = workspace.getConfiguration("files");
25+
const autoSave = config.get<string>("autoSave");
26+
return autoSave === "off";
27+
}
28+
29+
public static compileOnDocumentSave(): boolean {
30+
const config = workspace.getConfiguration("renpy");
31+
return config.get<boolean>("compileOnDocumentSave") === true;
32+
}
33+
34+
public static shouldWatchFoldersForChanges(): boolean {
35+
const config = workspace.getConfiguration("renpy");
36+
return config.get<boolean>("watchFoldersForChanges") === true;
37+
}
38+
39+
public static getRenpyExecutablePath(): string {
40+
const config = workspace.getConfiguration("renpy");
41+
return config.get<string>("renpyExecutableLocation") || "";
42+
}
43+
44+
private static excludeCompiledFilesConfig() {
45+
const renpyExclude = ["**/*.rpyc", "**/*.rpa", "**/*.rpymc", "**/cache/"];
46+
const config = workspace.getConfiguration("files");
47+
const workspaceExclude = config.inspect<WorkspaceConfiguration>("exclude");
48+
const exclude = { ...workspaceExclude?.workspaceValue };
49+
renpyExclude.forEach((element) => {
50+
if (!(element in exclude)) {
51+
Object.assign(exclude, { [element]: true });
52+
}
53+
});
54+
config.update("exclude", exclude, ConfigurationTarget.Workspace);
55+
}
56+
}

src/definition.ts

+15-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
1-
// Definition Provider
2-
"use strict";
3-
4-
import { Definition, Location, Position, TextDocument, Uri } from "vscode";
1+
// Provider for Go To Definition
2+
import { CancellationToken, Definition, Location, Position, ProviderResult, TextDocument, Uri, languages } from "vscode";
53
import { getKeywordPrefix } from "./extension";
64
import { rangeAsString } from "./navigation";
75
import { NavigationData } from "./navigation-data";
86
import { getFileWithPath, stripWorkspaceFromFile } from "./workspace";
97

8+
export const definitionProvider = languages.registerDefinitionProvider("renpy", {
9+
provideDefinition(document: TextDocument, position: Position, token: CancellationToken): ProviderResult<Definition> {
10+
if (token.isCancellationRequested) {
11+
return;
12+
}
13+
14+
return new Promise((resolve) => {
15+
resolve(getDefinition(document, position));
16+
});
17+
},
18+
});
19+
1020
export function getDefinition(document: TextDocument, position: Position): Definition | undefined {
1121
const range = document.getWordRangeAtPosition(position);
1222
if (!range) {
13-
return;
23+
return undefined;
1424
}
1525

1626
// check if this range is a semantic token

src/diagnostics.ts

-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
// Diagnostics (warnings and errors)
2-
"use strict";
3-
42
import { Diagnostic, DiagnosticCollection, DiagnosticSeverity, ExtensionContext, Range, TextDocument, window, workspace } from "vscode";
53
import { NavigationData } from "./navigation-data";
64
import { extractFilename } from "./workspace";

src/displayable.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
// Displayable Class
2-
"use strict";
32

43
export class Displayable {
54
name: string;

0 commit comments

Comments
 (0)