Service Autocomplete (code completions)

I quickly made a plugin using the brand new APIs to speedup my workflow in studio and decided to share!

quick video demo

Features

  • List service names by typing
    image

  • When a service is autocompleted from the above the service will be automatically added to the top of the script excluding comments
    image

  • Alphabetically sorts services and adds a line of padding between the last service and code below (thanks @grilme99 for the suggestion)
    image

  • Learn more button for Services both in completion and for variable completion (for some reason roblox doesn’t provide this by default)
    image

Videos of it in action
https://gyazo.com/1e2b1ded99c1fa3d88bf4cfa8fa591b5
https://gyazo.com/9b00a0c4134d83562823664a0d316469
https://gyazo.com/31a44161064f8ff4801cbf6c766e0055

if you run into issues not mentioned above bring them up in the comments and I’ll work on a fix

58 Likes

This is extremely useful! It gets very tiring to have to define the service at the top of the script each time.

Can you add support for ChatService?

It’s a service that’s not defined with :GetService, which is why it’s not included.

You have to define ChatService like this:

local ServerScriptService = game:GetService("ServerScriptService")
local ChatService = require(ServerScriptService:WaitForChild("ChatServiceRunner").ChatService)

Thanks for ur feedback but this plugin is only targeted towards services defined by roblox. So as of right now I don’t think I’ll be adding in the ChatServiceRunner or other instances that don’t come from game:GetService() try looking for a snippets plugin as of right now.

I might add it later but it’ll take a lot of work to the point I might as well make a plugin to define code snippets.

(which might not be a bad idea actually :bulb:)

2 Likes

This plugin is a godsend, would definitely recommend.
Also, is there any reason why “ModuleScript” is a service?

can you send a screenshot I can’t seem reproduce this

also the plugin quickly grabs all services when it starts (excluding roblox locked services)

I think I know the issue, I have a plugin that inserts 2 ModuleScripts under the DataModel. The autocomplete probably thought it was a service because it was parented to the DataModel. You may want to check if items under game are a service using if game:GetService("ServiceName") ~= nil.

yeah good catch I’ll change it.

Gonna rewrite this plugin in the upcoming days since initial release was basically just seeing if its possible.

1 Like

There are all really good plugins, but is it possible to opensource it so we can learn from this?

1 Like

The community needs more plugins like this one…

Dumped from source
local ScriptEditorService = game:GetService("ScriptEditorService")

-- service names is not ideal but causes security checks if not used so :/
local ServiceNames = {}

local CompletingDoc = nil
local CompleteingLine = 0
local CompleteingWordStart = 0

local PROCESSNAME = "Baileyeatspizza - Autocomplete Services"
local SINGLELINECOMMENT = "%-%-"
local COMMENTBLOCKSTART = "%-%-%[%["
local COMMENTBLOCKEND = "%]%]"
local LEARNMORELINK = "https://create.roblox.com/docs/reference/engine/classes/"
local SERVICEDEF = 'local %s = game:GetService("%s")\n'

type Request = {
	position: {
		line: number,
		character: number,
	},
	textDocument: {
		document: ScriptDocument?,
		script: LuaSourceContainer?,
	},
}

type ResponseItem = {
	label: string,
	kind: Enum.CompletionItemKind?,
	tags: { Enum.CompletionItemTag }?,
	detail: string?,
	documentation: {
		value: string,
	}?,
	overloads: number?,
	learnMoreLink: string?,
	codeSample: string?,
	preselect: boolean?,
	textEdit: {
		newText: string,
		replace: {
			start: { line: number, character: number },
			["end"]: { line: number, character: number },
		},
	}?,
}

type Response = {
	items: {
		[number]: ResponseItem,
	},
}

type DocChanges = {
	range: { start: { line: number, character: number }, ["end"]: { line: number, character: number } },
	text: string,
}

local function warnLog(message)
	warn("[Service Autofill] - " .. message)
end

local function isService(instance)
	-- not adding workspace due to the builtin globals
	if instance.ClassName == "Workspace" then
		return false
	end

	return game:GetService(instance.ClassName)
end

local function checkIfService(instance)
	local success, validService = pcall(isService, instance)
	if success and validService then
		ServiceNames[instance.ClassName] = true
	else
		pcall(function()
			ServiceNames[instance.ClassName] = false
		end)
	end
end

-- strings are irritating due to the three potential definitions
-- for performance if it looks close enough like the autofil is within the strings
-- it will just cancel out
-- Not accounting for multi line strings due to performance and how little they are used
local function backTraceStrings(doc: ScriptDocument, line: number, char: number)
	return false
end

local function backTraceComments(doc: ScriptDocument, line: number, char: number): boolean
	local startLine = doc:GetLine(line)
	local lineCount = doc:GetLineCount()

	-- single line comment blocks
	if string.find(startLine, COMMENTBLOCKSTART) then
		local commentBlockEnd = string.find(startLine, COMMENTBLOCKEND)

		if not commentBlockEnd or commentBlockEnd >= char then
			return true
		end
	elseif string.match(startLine, SINGLELINECOMMENT) then
		return true
	end

	-- exception if the comment block end is at the start of the line?
	local exceptionCase = string.find(startLine, COMMENTBLOCKEND)
	if exceptionCase and char >= exceptionCase then
		return false
	end

	local blockStart = nil
	local blockStartLine = nil
	local blockEnd = nil
	local blockEndLine = nil

	for i = line, 1, -1 do
		local currentLine = doc:GetLine(i)

		blockStart = string.find(currentLine, COMMENTBLOCKSTART)

		if blockStart then
			local sameLineBlockEnd = string.find(currentLine, COMMENTBLOCKEND)

			if sameLineBlockEnd then
				return false
			end
			blockStartLine = i

			-- do a quick search forward to find it

			for l = i + 1, lineCount do
				local nextLine = doc:GetLine(l)

				blockEnd = string.find(nextLine, COMMENTBLOCKEND)

				if blockEnd then
					blockEndLine = l
					break
				end
			end

			break
		end
	end

	if not blockStart or not blockEnd then
		return false
	end

	if line > blockStartLine and line <= blockEndLine then
		return true
	end

	return false
end

local function hasBackTraces(doc, line, char)
	if backTraceComments(doc, line, char) then
		return true
	end
	if backTraceStrings(doc, line, char) then
		return true
	end

	return false
end

-- used in a different function so it can return without ruining the callback
local function addServiceAutocomplete(request: Request, response: Response)
	local doc = request.textDocument.document

	if hasBackTraces(doc, request.position.line, request.position.character) then
		return
	end

	local req = doc:GetLine(request.position.line)

	req = string.sub(req, 1, request.position.character - 1)

	local requestedWord = string.match(req, "[%w]+$")

	local statementStart, variableStatement = string.find(req, "local " .. (requestedWord or ""))

	if variableStatement and #string.sub(req, statementStart, variableStatement) >= #req then
		return
	end

	-- no text found
	if requestedWord == nil then
		return
	end

	local beforeRequest = string.sub(req, 1, #req - #requestedWord)

	if string.sub(beforeRequest, #beforeRequest, #beforeRequest) == "." then
		return
	end

	-- TODO: improve with better string checks
	if string.match(beforeRequest, "'") or string.match(beforeRequest, '"') then
		return
	end

	local potentialMatches = {}

	for serviceName in ServiceNames do
		if string.sub(string.lower(serviceName), 1, #requestedWord) == string.lower(requestedWord) then
			potentialMatches[serviceName] = true
		end
	end

	for _, v in response.items do
		-- already exists as an autofill
		-- likely that its defined
		if potentialMatches[v.label] then
			-- append a leanMoreLink to the builtin one (this is embarassing LOL)
			v.learnMoreLink = LEARNMORELINK .. v.label
			potentialMatches[v.label] = nil
		end
	end

	for serviceName in potentialMatches do
		local field: ResponseItem = {
			label = serviceName,
			detail = "Get Service " .. serviceName,
			learnMoreLink = LEARNMORELINK .. serviceName,
		}

		table.insert(response.items, field)
	end

	-- don't update if theres no matches
	if next(potentialMatches) == nil then
		return
	end

	CompletingDoc = doc
	CompleteingLine = request.position.line
	CompleteingWordStart = string.find(req, requestedWord, #req - #requestedWord)
end

local function completionRequested(request: Request, response: Response)
	local doc = request.textDocument.document
	-- can't write to the command bar sadly ;C
	if doc == nil or doc:IsCommandBar() then
		return response
	end

	CompleteingLine = 0
	CompleteingWordStart = 0
	addServiceAutocomplete(request, response)

	return response
end

local function closeThread()
	task.defer(task.cancel, coroutine.running())
	task.wait(5)
end

local function findAllServices(doc: ScriptDocument, startLine: number?): { [string]: number }?
	startLine = startLine or 1
	local lineCount = doc:GetLineCount()

	-- we don't account for duplicate services
	-- that is user error if it occurs
	local services = {
		--[ServiceName] = lineNumber
	}

	for i = startLine :: number, lineCount do
		local line = doc:GetLine(i)
		local match = string.match(line, ":GetService%([%C]+")

		if match then
			local closingParenthesis = string.find(match, "%)")
			match = string.sub(match, 14, closingParenthesis - 2)

			services[match] = i
		end
	end

	if next(services) then
		return services
	end

	-- required unfortunately
	return nil
end

local function findNonCommentLine(doc: ScriptDocument)
	local lineAfterComments = 0
	local comments = true

	local lineCount = doc:GetLineCount()

	for i = 1, lineCount do
		local line = doc:GetLine(i)
		if string.match(line, COMMENTBLOCKSTART) then
			local foundBlockEnd = false
			if string.match(line, COMMENTBLOCKEND) then
				foundBlockEnd = true
			end

			if not foundBlockEnd then
				for l = i, lineCount do
					local nextLine = doc:GetLine(l)
					if string.match(nextLine, COMMENTBLOCKEND) then
						foundBlockEnd = true
						lineAfterComments = l
						break
					end
				end
			end

			if not foundBlockEnd then
				warnLog("Couldn't find end of comment block missing: ]]")
				closeThread()
			end
		elseif string.match(line, SINGLELINECOMMENT) then
			comments = true
		else
			comments = false
		end

		if lineAfterComments < i then
			if not comments then
				break
			end
			lineAfterComments = i
		end
	end

	return lineAfterComments + 1
end

local function processDocChanges(doc: ScriptDocument, change: DocChanges)
	if change.range.start.character ~= CompleteingWordStart and change.range.start.line ~= CompleteingLine then
		return
	end

	local serviceName = change.text

	if not ServiceNames[serviceName] or #serviceName < 3 then
		return
	end

	-- for some reason studio ignored the variable on the top line so exit if it exists
	local firstLineService = doc:GetLine(1)
	local topService = string.match(firstLineService, "%w+", 6)
	if serviceName == topService then
		return
	end

	CompleteingLine = 0
	CompleteingWordStart = 0

	local firstServiceLine = 99999
	local lastServiceLine = 1
	local lineToComplete = 1

	local existingServices = findAllServices(doc)
	if existingServices then
		for otherService, line in existingServices do
			if line > lineToComplete then
				if serviceName > otherService then
					lineToComplete = line
				end

				-- hit a bug where its trying to dup a service
				if otherService == serviceName then
					return
				end

				lastServiceLine = line
			end

			if line < firstServiceLine then
				firstServiceLine = line
			end
		end

		-- caused too many problems
		for _, line in existingServices do
			if line > lastServiceLine then
				lastServiceLine = line
			end
		end

		-- hasn't changed default to the lowest
		if lineToComplete == 1 then
			lineToComplete = firstServiceLine - 1
		end

		lineToComplete += 1
		lastServiceLine += 1
	else
		lineToComplete = findNonCommentLine(doc)
	end

	if lastServiceLine == 1 then
		lastServiceLine = lineToComplete + 1
	end

	if lastServiceLine >= doc:GetLineCount() then
		lastServiceLine = doc:GetLineCount()
	end

	if doc:GetLine(lastServiceLine) ~= "" then
		doc:EditTextAsync("\n", lastServiceLine, 1, 0, 0)
	end

	local serviceRequire = string.format(SERVICEDEF, serviceName, serviceName)
	doc:EditTextAsync(serviceRequire, lineToComplete, 1, 0, 0)
end

local function onDocChanged(doc: ScriptDocument, changed: { DocChanges })
	if doc:IsCommandBar() then
		return
	end

	if doc ~= CompletingDoc then
		return
	end

	for _, change in changed do
		processDocChanges(doc, change)
	end
end

-- prevent potential overlap for some reason errors if one doesn't exist weird api choice but ok-
pcall(function()
	ScriptEditorService:DeregisterAutocompleteCallback(PROCESSNAME)
end)
ScriptEditorService:RegisterAutocompleteCallback(PROCESSNAME, 69, completionRequested)
ScriptEditorService.TextDocumentDidChange:Connect(onDocChanged)

game.ChildAdded:Connect(checkIfService)
game.ChildRemoved:Connect(checkIfService)
for _, v in game:GetChildren() do
	checkIfService(v)
end

Yeah sorry, its a bit messy as of right now (all crammed into 1 file) I’ll make a GitHub repo with the next major update.

The Plugin received an update recently to use a Lexer maintained by @boatbomber check that out here

Recently I’ve come to the late realization that auto-complete could probably be applied when Roblox is showing the buildin globals and related (might migrate to that at a later date if applicable)

But as requested by @mrtouie and @ducksandwifi source code: GitHub - Baileyeatspizza/Service-Autocomplete: Roblox studio port of service autocompletion roblox LSP provides

Feel free to submit issues and pull requests or just reply to this thread.

3 Likes

very cool plugin ! Save a lot of times. Could you make that when you finished typing it, it will remove the text you put, for example, I type ServerScriptService, it will put the variable but it wont remove what I write.

This will save me alot time! Neat plugin you’ve made!
(Also what is the name of the font you’re using?)
(edit: found the font, its called “JetBrains Mono”)

1 Like

Plugin is currently faulty

The plugin has stopped working recently likely due to this flag “FFlagAutocompleteReplaceOnlyOnTab” being set to true

A temporary solution is to press tab to autocomplete a service. I know this isn’t ideal.

Unsure why roblox is now treating autocomplete differently based on if the user pressed tab to complete or not.

Overall, I’m going to wait a week or two if nothing changes then I’ll update the plugin to find and override the incorrect text (potentially dangerous). Hopefully roblox will revert this change :pray:.

Heres an example to breakdown what im talking about


In the past this would be put the two service definitions in correctly.

Now replicated storage is defined correctly but the “replicatedsto” text used to obtain the autocomplete is not replaced (it should be). The ReplicatedStorage text is put a line below instead (the plugin inserts a line of padding)

The same happens for “replicatedfi” which should be changed to ReplicatedFirst but instead its somehow thrown into the line below causing faulty code.

Again plugin will work perfectly fine if you press tab to autocomplete

6 Likes

Is it possible to get the sauce?
I would like to create my own auto complete suggestions but the wiki is rather confusing…

I was about to post about this because this plugin has been extremely useful.

I’ve been using this plugin for a year now and its really great. I hope this gets fixed soon.

1 Like

Do you think you can add another order for these such as they order by name length?
For example:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local RunService = game:GetService("RunService")

Sorry the plugin is currently broken so I’m not going to try and implement this right now.

But yeah seems pretty easy I’ll look into doing it afterwards the hard part is coming up with a way to do settings for the plugin it’d be weird having a UI / Top button for just 1 option. So I’ll probably make it something like (_G or Shared) .AutoComplete:SetOrderType(…) along those lines.

But as it stands the plugin won’t reorganise existing services out of general fear of damaging the script and I dont plan on dealing with the headache that comes with that so it’ll just insert where it should go like its already doing

2 Likes