diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a24a837f9..bcae864ba 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -59,8 +59,8 @@ jmh = "1.37" # Build tools cctJavadoc = "1.8.4" -checkstyle = "10.21.4" -errorProne-core = "2.37.0" +checkstyle = "10.23.1" +errorProne-core = "2.38.0" errorProne-plugin = "4.1.0" fabric-loom = "1.10.4" githubRelease = "2.5.2" @@ -70,7 +70,7 @@ illuaminate = "0.1.0-83-g1131f68" lwjgl = "3.3.3" minotaur = "2.8.7" modDevGradle = "2.0.78" -nullAway = "0.12.4" +nullAway = "0.12.7" shadow = "8.3.1" spotless = "7.0.2" taskTree = "2.1.1" diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/KeyConverter.java b/projects/common/src/client/java/dan200/computercraft/client/gui/KeyConverter.java new file mode 100644 index 000000000..f99a254bf --- /dev/null +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/KeyConverter.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.client.gui; + +import org.lwjgl.glfw.GLFW; + +/** + * Supports for converting/translating key codes. + */ +public class KeyConverter { + /** + * GLFW's key events refer to the physical key code, rather than the "actual" key code (with keyboard layout + * applied). + *

+ * This makes sense for WASD-style input, but is a right pain for keyboard shortcuts — this function attempts to + * translate those keys back to their "actual" key code. See also + * this discussion on GLFW's GitHub. + * + * @param key The current key code. + * @param scanCode The current scan code. + * @return The translated key code. + */ + public static int physicalToActual(int key, int scanCode) { + var name = GLFW.glfwGetKeyName(key, scanCode); + if (name == null || name.length() != 1) return key; + + // If we've got a single character as the key name, treat that as the ASCII value of the key, + // and map that back to a key code. + var character = name.charAt(0); + + // 0-9 and A-Z map directly to their GLFW key (they're the same ASCII code). + if ((character >= '0' && character <= '9') || (character >= 'A' && character <= 'Z')) return character; + // a-z map to GLFW_KEY_{A,Z} + if (character >= 'a' && character <= 'z') return GLFW.GLFW_KEY_A + (character - 'a'); + + return key; + } +} diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java b/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java index 49110157e..055b2b13f 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/widgets/TerminalWidget.java @@ -4,6 +4,7 @@ package dan200.computercraft.client.gui.widgets; +import dan200.computercraft.client.gui.KeyConverter; import dan200.computercraft.client.render.RenderTypes; import dan200.computercraft.client.render.text.FixedWidthFontRenderer; import dan200.computercraft.core.terminal.Terminal; @@ -83,7 +84,7 @@ public class TerminalWidget extends AbstractWidget { } if ((modifiers & GLFW.GLFW_MOD_CONTROL) != 0) { - switch (key) { + switch (KeyConverter.physicalToActual(key, scancode)) { case GLFW.GLFW_KEY_T -> { if (terminateTimer < 0) terminateTimer = 0; } @@ -119,7 +120,7 @@ public class TerminalWidget extends AbstractWidget { computer.keyUp(key); } - switch (key) { + switch (KeyConverter.physicalToActual(key, scancode)) { case GLFW.GLFW_KEY_T -> terminateTimer = -1; case GLFW.GLFW_KEY_R -> rebootTimer = -1; case GLFW.GLFW_KEY_S -> shutdownTimer = -1; diff --git a/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java b/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java index 52ecac60c..51cf53b11 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java +++ b/projects/common/src/client/java/dan200/computercraft/client/render/TurtleBlockEntityRenderer.java @@ -62,8 +62,8 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer whence, Optional offset) throws LuaException { checkOpen(); @@ -179,6 +180,8 @@ public abstract class AbstractHandle { * @throws LuaException If the file has been closed. * @cc.treturn string|nil The remaining contents of the file, or {@code nil} in the event of an error. * @cc.since 1.80pr1 + * @cc.changed 1.109.0 Binary-mode handles are now consistent with non-binary files, and return an empty string at + * the end of the file, rather than {@code nil}. */ public Object @Nullable [] readAll() throws LuaException { checkOpen(); diff --git a/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java b/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java index 234127244..2916929f1 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java +++ b/projects/core/src/main/java/dan200/computercraft/core/util/StringUtil.java @@ -120,7 +120,7 @@ public final class StringUtil { var idx = 0; var iterator = clipboard.codePoints().iterator(); - while (iterator.hasNext() && idx <= output.length) { + while (iterator.hasNext() && idx < output.length) { var chr = unicodeToTerminal(iterator.next()); if (chr < 0) continue; // Strip out unconvertible characters if (!isTypableChar(chr)) break; // Stop at untypable ones. diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/colors.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/colors.lua index b98ff8d75..f013a29e1 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/colors.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/colors.lua @@ -371,7 +371,7 @@ function toBlit(color) local hex = color_hex_lookup[color] if hex then return hex end - if color < 0 or color > 0xffff then error("Colour out of range", 2) end + if color < 1 or color > 0xffff then error("Colour out of range", 2) end return string.format("%x", math.floor(math.log(color, 2))) end diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua index 7be7aebaf..5b5678d63 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua @@ -65,7 +65,7 @@ local function parse_color(color) return expect(1, color, "number") end - if color < 0 or color > 0xffff then error("Colour out of range", 3) end + if color < 1 or color > 0xffff then error("Colour out of range", 3) end return 2 ^ math.floor(math.log(color, 2)) end diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/lexer.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/lexer.lua index 9faf3b5a9..2d8d91938 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/lexer.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/cc/internal/syntax/lexer.lua @@ -238,7 +238,7 @@ local function lex_token(context, str, pos) if end_pos then return tokens.STRING, end_pos end context.report(errors.unfinished_long_string, pos, boundary_pos, boundary_pos - pos) - return tokens.ERROR, #str + return tokens.STRING, #str elseif pos + 1 == boundary_pos then -- Just a "[" return tokens.OSQUARE, pos else -- Malformed long string, for instance "[=" @@ -260,7 +260,7 @@ local function lex_token(context, str, pos) if end_pos then return tokens.COMMENT, end_pos end context.report(errors.unfinished_long_comment, pos, boundary_pos, boundary_pos - comment_pos) - return tokens.ERROR, #str + return tokens.COMMENT, #str end end diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/edit.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/edit.lua index e05ec1766..0e7a131e8 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/edit.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/edit.lua @@ -175,63 +175,62 @@ local function save(_sPath, fWrite) return ok, err, fileerr end -local tKeywords = { - ["and"] = true, - ["break"] = true, - ["do"] = true, - ["else"] = true, - ["elseif"] = true, - ["end"] = true, - ["false"] = true, - ["for"] = true, - ["function"] = true, - ["if"] = true, - ["in"] = true, - ["local"] = true, - ["nil"] = true, - ["not"] = true, - ["or"] = true, - ["repeat"] = true, - ["return"] = true, - ["then"] = true, - ["true"] = true, - ["until"] = true, - ["while"] = true, -} -local function tryWrite(sLine, regex, colour) - local match = string.match(sLine, regex) - if match then - if type(colour) == "number" then - term.setTextColour(colour) - else - term.setTextColour(colour(match)) - end - term.write(match) - term.setTextColour(textColour) - return string.sub(sLine, #match + 1) - end - return nil +local tokens = require "cc.internal.syntax.parser".tokens +local lex_one = require "cc.internal.syntax.lexer".lex_one + +local token_colours = { + [tokens.STRING] = stringColour, + [tokens.COMMENT] = commentColour, + -- Keywords + [tokens.AND] = keywordColour, + [tokens.BREAK] = keywordColour, + [tokens.DO] = keywordColour, + [tokens.ELSE] = keywordColour, + [tokens.ELSEIF] = keywordColour, + [tokens.END] = keywordColour, + [tokens.FALSE] = keywordColour, + [tokens.FOR] = keywordColour, + [tokens.FUNCTION] = keywordColour, + [tokens.GOTO] = keywordColour, + [tokens.IF] = keywordColour, + [tokens.IN] = keywordColour, + [tokens.LOCAL] = keywordColour, + [tokens.NIL] = keywordColour, + [tokens.NOT] = keywordColour, + [tokens.OR] = keywordColour, + [tokens.REPEAT] = keywordColour, + [tokens.RETURN] = keywordColour, + [tokens.THEN] = keywordColour, + [tokens.TRUE] = keywordColour, + [tokens.UNTIL] = keywordColour, + [tokens.WHILE] = keywordColour, +} +-- Fill in the remaining tokens. +for _, token in pairs(tokens) do + if not token_colours[token] then token_colours[token] = textColour end end -local function writeHighlighted(sLine) - while #sLine > 0 do - sLine = - tryWrite(sLine, "^%-%-%[%[.-%]%]", commentColour) or - tryWrite(sLine, "^%-%-.*", commentColour) or - tryWrite(sLine, "^\"\"", stringColour) or - tryWrite(sLine, "^\".-[^\\]\"", stringColour) or - tryWrite(sLine, "^\'\'", stringColour) or - tryWrite(sLine, "^\'.-[^\\]\'", stringColour) or - tryWrite(sLine, "^%[%[.-%]%]", stringColour) or - tryWrite(sLine, "^[%w_]+", function(match) - if tKeywords[match] then - return keywordColour - end - return textColour - end) or - tryWrite(sLine, "^[^%w_]", textColour) +local lex_context = { line = function() end, report = function() end } + +local function writeHighlighted(line) + local pos, colour = 1, nil + + while true do + local token, _, finish = lex_one(lex_context, line, pos) + if not token then break end + + local new_colour = token_colours[token] + if new_colour ~= colour then + term.setTextColor(new_colour) + colour = new_colour + end + + term.write(line:sub(pos, finish)) + pos = finish + 1 end + + term.write(line:sub(pos)) end local tCompletions @@ -352,7 +351,7 @@ local tMenuFuncs = { if bReadOnly then set_status("Access denied", false) else - local ok, _, fileerr = save(sPath, function(file) + local ok, _, fileerr = save(sPath, function(file) for _, sLine in ipairs(tLines) do file.write(sLine .. "\n") end @@ -547,7 +546,7 @@ local function acceptCompletion() -- Append the completion local sCompletion = tCompletions[nCompletion] tLines[y] = tLines[y] .. sCompletion - setCursor(x + #sCompletion , y) + setCursor(x + #sCompletion, y) end end @@ -805,7 +804,7 @@ while bRunning do -- Input text local sLine = tLines[y] tLines[y] = string.sub(sLine, 1, x - 1) .. param .. string.sub(sLine, x) - setCursor(x + #param , y) + setCursor(x + #param, y) end elseif sEvent == "mouse_click" then diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/startup.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/startup.lua index f3b9f7a08..a14e999ef 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/startup.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/startup.lua @@ -191,7 +191,7 @@ end -- Show MOTD if settings.get("motd.enable") then - shell.run("motd") + shell.run("/rom/programs/motd") end -- Run the user created startup, either from disk drives or the root diff --git a/projects/core/src/test/java/dan200/computercraft/core/util/StringUtilTest.java b/projects/core/src/test/java/dan200/computercraft/core/util/StringUtilTest.java new file mode 100644 index 000000000..69e9d3c07 --- /dev/null +++ b/projects/core/src/test/java/dan200/computercraft/core/util/StringUtilTest.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.util; + +import dan200.computercraft.api.lua.LuaValues; +import dan200.computercraft.test.core.ReplaceUnderscoresDisplayNameGenerator; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DisplayNameGeneration(ReplaceUnderscoresDisplayNameGenerator.class) +class StringUtilTest { + @ParameterizedTest + @ValueSource(strings = { "hello\nworld", "hello\n\rworld", "hello\rworld" }) + public void getClipboardString_returns_a_single_line(String input) { + var result = StringUtil.getClipboardString(input); + assertEquals(LuaValues.encode("hello"), result); + } + + @Test + public void getClipboardString_limits_length() { + var input = "abcdefghijklmnop".repeat(50); + var result = StringUtil.getClipboardString(input); + assertEquals(StringUtil.MAX_PASTE_LENGTH, result.limit()); + + assertEquals( + LuaValues.encode(input.substring(0, StringUtil.MAX_PASTE_LENGTH)), + result + ); + } +} diff --git a/projects/core/src/test/resources/test-rom/spec/apis/colors_spec.lua b/projects/core/src/test/resources/test-rom/spec/apis/colors_spec.lua index 46c77dcf8..8f50b0d56 100644 --- a/projects/core/src/test/resources/test-rom/spec/apis/colors_spec.lua +++ b/projects/core/src/test/resources/test-rom/spec/apis/colors_spec.lua @@ -94,6 +94,7 @@ describe("The colors library", function() end) it("errors on out-of-range colours", function() + expect.error(colors.toBlit, 0):eq("Colour out of range") expect.error(colors.toBlit, -120):eq("Colour out of range") expect.error(colors.toBlit, 0x10000):eq("Colour out of range") end) diff --git a/projects/core/src/test/resources/test-rom/spec/apis/window_spec.lua b/projects/core/src/test/resources/test-rom/spec/apis/window_spec.lua index 89e4e2993..4ad3d797c 100644 --- a/projects/core/src/test/resources/test-rom/spec/apis/window_spec.lua +++ b/projects/core/src/test/resources/test-rom/spec/apis/window_spec.lua @@ -59,6 +59,16 @@ describe("The window library", function() expect.error(w.setTextColour, nil):eq("bad argument #1 (number expected, got nil)") expect.error(w.setTextColour, -5):eq("Colour out of range") + expect.error(w.setTextColour, 0):eq("Colour out of range") + expect.error(w.setTextColour, 0x10000):eq("Colour out of range") + end) + + it("accepts valid colours", function() + local w = mk() + for i = 0, 15 do + w.setBackgroundColour(2 ^ i) + expect(w.getBackgroundColour()):eq(2 ^ i) + end end) it("supports invalid combined colours", function() diff --git a/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md b/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md index 9d5af0531..e73c4f62f 100644 --- a/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md +++ b/projects/core/src/test/resources/test-rom/spec/modules/cc/internal/syntax/lexer_spec.md @@ -67,7 +67,7 @@ This comment was never finished. 1 | --[=[ | ^^^^^ Comment was started here. We expected a closing delimiter (]=]) somewhere after this comment was started. -1:1-1:5 ERROR --[=[ +1:1-1:5 COMMENT --[=[ ``` Nested comments are rejected, just as Lua 5.1 does: @@ -191,7 +191,7 @@ This string was never finished. 1 | return [[ | ^^ String was started here. We expected a closing delimiter (]]) somewhere after this string was started. -1:8-1:9 ERROR [[ +1:8-1:9 STRING [[ ``` We also handle malformed opening strings: