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.
There are all really good plugins, but is it possible to opensource it so we can learn from this?
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.
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”)
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 .
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
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.
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
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.
Fixed the plugin 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
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”.