Realtime RichText Lua Syntax Highlighting

Hello! Recently I’ve been working on this little project of mine that allows for real time syntax highlighting in game! I think it’s real cool and would like to share it with all of you!

RobloxStudioBeta_DJP4B4TXB6
(I did this by overlaying a TextLabel with RichText enabled, on top of TextBox and having it update the TextLabel with what was in the TextBox)

I have also made this module open source! (feel free to use it in any of your projects!)
Below is the module’s source.

local highlighter = {}
local keywords = {
	lua = {
		"and", "break", "or", "else", "elseif", "if", "then", "until", "repeat", "while", "do", "for", "in", "end",
		"local", "return", "function", "export"
	},
	rbx = {
		"game", "workspace", "script", "math", "string", "table", "task", "wait", "select", "next", "Enum",
		"error", "warn", "tick", "assert", "shared", "loadstring", "tonumber", "tostring", "type",
		"typeof", "unpack", "print", "Instance", "CFrame", "Vector3", "Vector2", "Color3", "UDim", "UDim2", "Ray", "BrickColor",
		"OverlapParams", "RaycastParams", "Axes", "Random", "Region3", "Rect", "TweenInfo",
		"collectgarbage", "not", "utf8", "pcall", "xpcall", "_G", "setmetatable", "getmetatable", "os", "pairs", "ipairs"
	},
	operators = {
		"#", "+", "-", "*", "%", "/", "^", "=", "~", "=", "<", ">", ",", ".", "(", ")", "{", "}", "[", "]", ";", ":"
	}
}

local colors = {
	numbers = Color3.fromRGB(255, 198, 0),
	boolean = Color3.fromRGB(214, 128, 23),
	operator = Color3.fromRGB(232, 210, 40),
	lua = Color3.fromRGB(160, 87, 248),
	rbx = Color3.fromRGB(146, 180, 253),
	str = Color3.fromRGB(56, 241, 87),
	comment = Color3.fromRGB(103, 110, 149),
	null = Color3.fromRGB(79, 79, 79),
	call = Color3.fromRGB(130, 170, 255),
	self_call = Color3.fromRGB(227, 201, 141),
	local_color = Color3.fromRGB(199, 146, 234),
	function_color = Color3.fromRGB(241, 122, 124),
	self_color = Color3.fromRGB(146, 134, 234),
	local_property = Color3.fromRGB(129, 222, 255),
}

local function createKeywordSet(keywords)
	local keywordSet = {}
	for _, keyword in ipairs(keywords) do
		keywordSet[keyword] = true
	end
	return keywordSet
end

local luaSet = createKeywordSet(keywords.lua)
local rbxSet = createKeywordSet(keywords.rbx)
local operatorsSet = createKeywordSet(keywords.operators)

local function getHighlight(tokens, index)
	local token = tokens[index]

	if colors[token .. "_color"] then
		return colors[token .. "_color"]
	end

	if tonumber(token) then
		return colors.numbers
	elseif token == "nil" then
		return colors.null
	elseif token:sub(1, 2) == "--" then
		return colors.comment
	elseif operatorsSet[token] then
		return colors.operator
	elseif luaSet[token] then
		return colors.rbx
	elseif rbxSet[token] then
		return colors.lua
	elseif token:sub(1, 1) == "\"" or token:sub(1, 1) == "\'" then
		return colors.str
	elseif token == "true" or token == "false" then
		return colors.boolean
	end

	if tokens[index + 1] == "(" then
		if tokens[index - 1] == ":" then
			return colors.self_call
		end

		return colors.call
	end

	if tokens[index - 1] == "." then
		if tokens[index - 2] == "Enum" then
			return colors.rbx
		end

		return colors.local_property
	end
end

function highlighter.run(source)
	local tokens = {}
	local currentToken = ""
	
	local inString = false
	local inComment = false
	local commentPersist = false
	
	for i = 1, #source do
		local character = source:sub(i, i)
		
		if inComment then
			if character == "\n" and not commentPersist then
				table.insert(tokens, currentToken)
				table.insert(tokens, character)
				currentToken = ""
				
				inComment = false
			elseif source:sub(i - 1, i) == "]]" and commentPersist then
				currentToken ..= "]"
				
				table.insert(tokens, currentToken)
				currentToken = ""
				
				inComment = false
				commentPersist = false
			else
				currentToken = currentToken .. character
			end
		elseif inString then
			if character == inString and source:sub(i-1, i-1) ~= "\\" or character == "\n" then
				currentToken = currentToken .. character
				inString = false
			else
				currentToken = currentToken .. character
			end
		else
			if source:sub(i, i + 1) == "--" then
				table.insert(tokens, currentToken)
				currentToken = "-"
				inComment = true
				commentPersist = source:sub(i + 2, i + 3) == "[["
			elseif character == "\"" or character == "\'" then
				table.insert(tokens, currentToken)
				currentToken = character
				inString = character
			elseif operatorsSet[character] then
				table.insert(tokens, currentToken)
				table.insert(tokens, character)
				currentToken = ""
			elseif character:match("[%w_]") then
				currentToken = currentToken .. character
			else
				table.insert(tokens, currentToken)
				table.insert(tokens, character)
				currentToken = ""
			end
		end
	end
	
	table.insert(tokens, currentToken)

	local highlighted = {}
	
	for i, token in ipairs(tokens) do
		local highlight = getHighlight(tokens, i)

		if highlight then
			local syntax = string.format("<font color = \"#%s\">%s</font>", highlight:ToHex(), token:gsub("<", "&lt;"):gsub(">", "&gt;"))
			
			table.insert(highlighted, syntax)
		else
			table.insert(highlighted, token)
		end
	end

	return table.concat(highlighted)
end

return highlighter

I am hoping to one day improve this to have autocompleting and alike, but I don’t think I could tackle something that challenging and complex (idk it might be real simple and I just don’t know it).

Thank you for reading and have a wonderful day!

48 Likes

Thanks you helped me. I tried to make my syntax highlight but it not working. I used ODE but it bugged.

3 Likes

You cooked here kind sir. I hope you don’t mind me using this on my fairly small game (5-20 concurrent players nowadays)

2 Likes

Hm, does this support multiline comments?

--[[simple
multiline]]
--[==[ a bit
not that
simple
multiline
--]==]
--[========[ and yes such comments work with any amount of "="
But they should match in quantity. So 3:3 will work, but not 3:5
--]========]
2 Likes

Yes, but it only supports basic multiline comments. (--[[multine comment here]])
To be fair, I didn’t know you could multiline comment like this --[==[another multiline comment]==] lol

It should be pretty simple to modify my script to support these strange, multiline comment variants though!

2 Likes

Thank you! (bit odd that people are just now seeing this topic from over 7 months ago lol)
I’ll check out that game you used it in, feel free to ask for help if you encounter any errors while using it

1 Like

They exist because [[ ]] can be met within script frequently:

SomeArray[ArrayOfIndexes[x]]

Using regular multiline will do this:

--[[
SomeArray[ArrayOfIndexes[x]]
]] ← error here
--[==[
SomeArray[ArrayOfIndexes[x]]
]==] Here, error won't occur. But cuz devforum missed such comments aswell
as you, we receive such syntax problems. Normal scripts will have everything
commented well
1 Like

@NiceBuild1 i have a question I did what I had to do but I cant get the mouse to show where you hovering over to type.

1 Like

or can you describe on what properties did you use for the label and textbox

1 Like

Hello, i’m sorry being stupid, but how could you implement this into a textbox? Nevermind, i found out.

wdym (30 letter cap :))))))))))))))))))))))))))))))

unpack, collectgarbage and such are not rbx exclusive, why are they are not in the lua table?

It might be possible to auto complete

Just loop trough the text into the text box and let it search for like keywords and if it match a keyword change that into the text propertie

Can you possibly provide an example of this in action? i cant seem to figure out on how to get this to work

1 Like

Hello! I don’t know if this works, because I haven’t tested it, but it might work.

local richtexttextlabel = script.Parent.box
local thing = require(script.Parent.highlighter)
richtexttextlabel.Text = thing.run(script.Parent.TextBox)
1 Like

Yea I already figured that out a few days ago. Thanks anyways.

i get attempt to get length of a Instance value

nervermind i fixed it aaaaaaaaaaaaaaaaaaaaa