Service Autocomplete (code completions)

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.

1 Like

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

2 Likes

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.

1 Like

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.

4 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

7 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

3 Likes

Maybe look to see how sleitnick solved the problem with his module autocomplete plugin?

The difference is hes replacing what the user inputs so for example :TestMod becomes local TestModule = require(…TestModule) etc my plugin isn’t doing this.

Also his plugin isn’t inserting any new lines which I believe is the reason my plugin is broke at the moment.

I’m gonna give it until next wednesdays update and take it from there. But as far as im concerned the problem my plugin is facing is a script error bug where for some reason inserting a new line is now making the auto complete prompt appear at the beginning of the script.

If u press tab to insert the plugin will work perfectly at the moment.

1 Like

Fixed the plugin :tada: consider enabling auto update for the future

Also fixed a bug where the plugin wouldn’t insert into the correct position

Added support for inserting based on the length of the service as requested by @SomeFedoraGuy:

Change how the plugin inserts new services by running Shared.ServiceSortType()

only two modes supported right now
Shared.ServiceSortType(“Alphabetical”)
Shared.ServiceSortType(“Length”)
these functions only need to be run once and will be saved to the local machine for future use.

Link to the github: GitHub - Baileyeatspizza/Service-Autocomplete: Roblox studio port of service autocompletion roblox LSP provides

3 Likes

Great update, thanks!

What I originally asked for would be a “LengthInversed” here, which, in your screenshot, would start at and “ServerScriptService” end at “Teams”.

1 Like

Ahh that makes a lot more sense lol

Here you go:

Just quickly update the plugin

1 Like