Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --!native
- --!optimize 2
- local ScriptEditorService = game:GetService("ScriptEditorService")
- local TextService = game:GetService("TextService")
- local Selection = game:GetService("Selection")
- local root = script.Parent
- if not require(root.VersionCheck)(plugin) then return end
- local AutocompleteName = "Variable Buddy"
- local AutocompletePriority = 1000
- 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: { ResponseItem },
- }
- type ScriptChange = {
- range: {
- start: { line: number, character: number },
- ["end"]: { line: number, character: number }
- },
- text: string
- }
- local fastFzy = require(root.FastFzy)
- local fastFzyConfig = fastFzy.CreateConfiguration({
- CaseSensitive = false
- })
- local services = table.freeze({
- "AnalyticsService",
- "AssetService",
- "BadgeService",
- "ChangeHistoryService",
- "Chat",
- "CollectionService",
- "ContentProvider",
- "ContextActionService",
- "CoreGui",
- "DataStoreService",
- "Debris",
- "DraggerService",
- "FriendService",
- "GamePassService",
- "GamepadService",
- "GeometryService",
- "GroupService",
- "GuiService",
- "HapticService",
- "HttpService",
- "InsertService",
- "Lighting",
- "LocalizationService",
- "LogService",
- "MarketplaceService",
- "MaterialService",
- "MemoryStoreService",
- "PathfindingService",
- "PhysicsService",
- "Players",
- "PluginDebugService",
- "PluginGuiService",
- "PolicyService",
- "ProximityPromptService",
- "ReplicatedFirst",
- "ReplicatedStorage",
- "RunService",
- "ScriptContext",
- "ScriptEditorService",
- "ScriptService",
- "Selection",
- "ServerScriptService",
- "ServerStorage",
- "SharedTableRegistry",
- "SoundService",
- "StarterGui",
- "StarterPack",
- "StarterPlayer",
- "Stats",
- "StudioService",
- "Teams",
- "TeleportService",
- "TestService",
- "TextChatService",
- "TextService",
- "TweenService",
- "UserInputService",
- "VRService"
- })
- local moduleAncestors = table.freeze({
- workspace,
- game:GetService("ReplicatedFirst"),
- game:GetService("ReplicatedStorage"),
- game:GetService("ServerScriptService"),
- game:GetService("ServerStorage"),
- game:GetService("StarterGui"),
- game:GetService("StarterPack"),
- game:GetService("StarterPlayer")
- })
- local modules = {}
- local connections = {}
- local items: {ResponseItem} = {}
- local function wrapPath(path)
- local parts = {}
- local index = 1
- while index <= #path do
- local start, stop, match = path:find("(%b[])", index)
- if start then
- if start > index then
- for part in path:sub(index, start - 1):gmatch("[^%.]+") do
- parts[#parts + 1] = part
- end
- end
- parts[#parts + 1] = match
- index = stop + 1
- else
- for part in path:sub(index):gmatch("[^%.]+") do
- parts[#parts + 1] = part
- end
- break
- end
- end
- local function needsWrapping(part)
- if part:match('^%[".*"%]$') then
- return false
- end
- return not part:match("^[_%a][_%w]*$")
- end
- for index, part in ipairs(parts) do
- if needsWrapping(part) then
- part = part:gsub('^%["(.*)"%]$', '%1')
- parts[index] = string.format('["%s"]', part)
- end
- end
- local result = {}
- for _, part in ipairs(parts) do
- if part:match('^%[".*"%]$') then
- result[#result + 1] = part
- else
- result[#result + 1] = part
- end
- end
- return table.concat(result, "."):gsub('%.%["', '["')
- end
- local function unwrapPath(path)
- local parts = {}
- local index = 1
- while index <= #path do
- local start, stop, match = path:find("(%b[])", index)
- if start ~= nil then
- if start > index then
- for part in path:sub(index, start - 1):gmatch("[^%.]+") do
- parts[#parts + 1] = part
- end
- end
- parts[#parts + 1] = match
- index = stop + 1
- else
- for part in path:sub(index):gmatch("[^%.]+") do
- parts[#parts + 1] = part
- end
- break
- end
- end
- local function needsUnwrapping(part)
- if part:match('^%["(.*)"%]$') then
- local unwrapped = part:match('^%["(.*)"%]$')
- return unwrapped:match("^[_%a][_%w]*$")
- end
- return false
- end
- for index, part in ipairs(parts) do
- if needsUnwrapping(part) then
- parts[index] = part:match('^%["(.*)"%]$')
- end
- end
- return table.concat(parts, "."):gsub('%.%["', '["')
- end
- local function splitPath(reference)
- local segments = {}
- local index = 1
- local len = #reference
- while index <= len do
- local char = reference:sub(index, index)
- if char == '.' then
- index += 1
- elseif char == '[' then
- if reference:sub(index, index + 1) == '[\"' then
- local endBracket = reference:find('"]', index)
- if endBracket then
- segments[#segments + 1] = reference:sub(index + 2, endBracket - 1)
- index = endBracket + 2
- end
- end
- else
- local nextDot = reference:find('%.', index)
- local nextBracket = reference:find('%[', index)
- local endPos = nextDot or len + 1
- if nextBracket and nextBracket < endPos then
- endPos = nextBracket
- end
- local segment = reference:sub(index, endPos - 1)
- segments[#segments + 1] = segment
- index = endPos
- end
- end
- return segments
- end
- local function getVariablePath(path, lines)
- path = splitPath(path)
- local newPath = ""
- for index = #path, 1, -1 do
- newPath = wrapPath(path[index].."."..newPath)
- for _, line in ipairs(lines) do
- local variablePath = line:match("local%s+[%w_]+%s*=%s*(.*)")
- if variablePath and unwrapPath(variablePath) == wrapPath(unwrapPath(line)) then
- return line:match("local%s+(%a+)%s*=%s*(.*)")
- end
- end
- end
- return newPath
- end
- local function getRelativePath(src, instance)
- local path = nil
- if src then
- if src:IsDescendantOf(instance) then
- path = "script.Parent"
- local parent = src
- while parent ~= instance and parent do
- parent = parent.Parent
- path ..= ".Parent"
- end
- elseif instance:IsDescendantOf(src) then
- local trace = {}
- local parent = instance
- while parent ~= src and parent do
- table.insert(trace, 1, parent.Name)
- parent = parent.Parent
- end
- path = `script.{table.concat(trace, ".")}`
- else
- path = instance:GetFullName():gsub('^Workspace.', "workspace.")
- end
- else
- path = instance:GetFullName():gsub('^Workspace.', "workspace.")
- end
- return wrapPath(path)
- end
- local currentScript = nil
- local function trackModule(module)
- if modules[module] then return end
- local connections = {}
- modules[module] = connections
- local moduleName = module.Name
- moduleName = moduleName:sub(1, 1):lower()..moduleName:sub(2, -1)
- local item = {
- label = moduleName,
- preselect = true,
- kind = Enum.CompletionItemKind.Module,
- tags = {Enum.CompletionItemTag.TypeCorrect},
- detail = getRelativePath(currentScript, module),
- module = module,
- textEdit = {
- newText = "",
- replace = {
- start = {},
- ["end"] = {},
- },
- },
- }
- items[#items + 1] = item
- connections[#connections + 1] = module:GetPropertyChangedSignal("Name"):Connect(function()
- item.label = module.Name
- item.detail = getRelativePath(currentScript, module)
- end)
- connections[#connections + 1] = module:GetPropertyChangedSignal("Parent"):Connect(function()
- item.detail = getRelativePath(currentScript, module)
- end)
- connections[#connections + 1] = module.Destroying:Connect(function()
- for _, connection in ipairs(connections) do
- connection:Disconnect()
- end
- modules[module] = nil
- table.remove(items, table.find(items, item))
- end)
- end
- local function handleInstance(instance)
- if not instance:IsA("ModuleScript") then return end
- trackModule(instance)
- end
- for _, instance in ipairs(moduleAncestors) do
- for _, instance in ipairs(instance:GetDescendants()) do
- handleInstance(instance)
- end
- connections[#connections + 1] = instance.DescendantAdded:Connect(handleInstance)
- end
- local function nameFormat(str)
- str = str:gsub("(%s+)(%a)", function(_, letter)
- return letter:upper()
- end):gsub("%s+", "")
- return str:sub(1, 1):lower()..str:sub(2, -1)
- end
- local function updateAutocompleteDetails()
- for _, item in ipairs(items) do
- if item.module == nil then continue end
- item.detail = getRelativePath(currentScript, item.module)
- end
- end
- ScriptEditorService:RegisterAutocompleteCallback(AutocompleteName, AutocompletePriority, function(request: Request, response: Response)
- local doc = request.textDocument.document :: ScriptDocument
- if doc == nil or request.textDocument.script == nil or doc:IsCommandBar() then
- currentScript = nil
- updateAutocompleteDetails()
- return response
- end
- currentScript = request.textDocument.script
- updateAutocompleteDetails()
- local line = doc:GetLine(request.position.line)
- if line:match("^%s*:") == nil then return response end
- local name = line:match("^:([^:]+)")
- local lines = request.textDocument.document:GetText(1, 1):split("\n")
- if not name then
- name = ""
- else
- name = name:lower()
- end
- local result = fastFzy.BetterFilter(fastFzyConfig, name, items :: FastFzy.HaystackItem)
- table.sort(result, function(a, b)
- return a.Score > b.Score
- end)
- for index, resultItem in ipairs(result) do
- local item = resultItem.HaystackItem
- if item.module then
- local module = item.module
- local relativePath = getRelativePath(currentScript, module)
- local finalPath = getVariablePath(relativePath, lines)
- item.path = relativePath
- local newText = `local {nameFormat(module.Name)} = require({finalPath})`
- local textEdit = item.textEdit
- textEdit.newText = newText
- textEdit.replace.start.line = request.position.line
- textEdit.replace.start.character = 1
- textEdit.replace["end"].line = request.position.line
- textEdit.replace["end"].character = #newText
- response.items[#response.items + 1] = item
- elseif item.serviceClass then
- local serviceClass = item.serviceClass
- local newText = `local {serviceClass} = game:GetService("{serviceClass}")`
- local textEdit = item.textEdit
- textEdit.newText = newText
- textEdit.replace.start.line = request.position.line
- textEdit.replace.start.character = 1
- textEdit.replace["end"].line = request.position.line
- textEdit.replace["end"].character = #newText
- response.items[#response.items + 1] = item
- end
- end
- return response
- end)
- local function findIndexSortingByLength(lines, newLine)
- local newLineWidth = TextService:GetTextSize(newLine, 14, Enum.Font.Code, Vector2.new(math.huge, math.huge)).X
- for index, line in ipairs(lines) do
- if newLineWidth > TextService:GetTextSize(line, 14, Enum.Font.Code, Vector2.new(math.huge, math.huge)).X then
- return index
- end
- end
- return #lines + 1
- end
- local function ensureService(doc: ScriptDocument, name)
- if table.find(services, name) == nil then return end
- local line = `local {name} = game:GetService("{name}")`
- local lines = doc:GetText():split("\n")
- if table.find(lines, line) then return end
- local services = {}
- for index, line in ipairs(lines) do
- if line:match('^%s*local%s+[%w_]+%s*=%s*game:GetService%("([%w_]+)"%);?$') then
- services[#services + 1] = line
- end
- end
- local index = findIndexSortingByLength(services, line)
- if index > #lines then index = #lines end
- doc:EditTextAsync(line.."\n", index, 1, index, 1)
- end
- local function findLongestSharedPath(lines)
- local paths = {}
- local minLength = math.huge
- local sharedLines = {}
- for index, line in ipairs(lines) do
- if line:find("game:GetService") then continue end
- local path = line:match("^%s*local%s+[%w_]+%s*=%s*require%((.*)%)") or line:match("^%s*local%s+[%w_]+%s*=%s*(.*)")
- if path then
- local split = splitPath(path)
- paths[#paths + 1] = split
- sharedLines[index] = line
- minLength = math.min(minLength, #split)
- end
- end
- if #paths < 2 then return end
- local commonPrefix = {}
- for index = 1, minLength, 1 do
- local currentPart = paths[1][index]
- local allMatch = true
- for _, path in pairs(paths) do
- if path[index] ~= currentPart then
- allMatch = false
- break
- end
- end
- if allMatch then
- commonPrefix[index] = currentPart
- else
- break
- end
- end
- if #commonPrefix == 0 then return end
- return wrapPath(table.concat(commonPrefix, ".")), sharedLines
- end
- local function escapePattern(text)
- local specialCharacters = {"%", ".", "(", ")", "[", "]", "+", "-", "*", "?", "^", "$"}
- for _, char in ipairs(specialCharacters) do
- text = text:gsub("%"..char, "%%"..char)
- end
- return text
- end
- local function getPathDestination(path)
- local bracketIndex = path:match("%[\"[^\"]*\"%]$")
- if bracketIndex then
- return bracketIndex:sub(3, -3)
- else
- local segments = {}
- for segment in string.gmatch(path, "[^%.]+") do
- table.insert(segments, segment)
- end
- local lastSegment = segments[#segments]
- local lastBracketed = lastSegment:match("%[\"[^\"]*\"%]$")
- if lastBracketed then
- return lastBracketed:sub(3, -3)
- else
- return lastSegment
- end
- end
- end
- local function optimizePathReferences(doc: ScriptDocument)
- local lastSharedPath = nil
- while true do
- local lines = doc:GetText():split("\n")
- local sharedPath, lines = findLongestSharedPath(lines)
- if not sharedPath then break end
- lastSharedPath = sharedPath
- local variable = nil
- for index, line in pairs(lines) do
- local path = line:match("local%s+[%w_]+%s*=%s*require%((.*)%)") or line:match("local%s+[%w_]+%s*=%s*(.*)")
- if path and unwrapPath(path) == sharedPath then
- variable = line:match("^local%s+([%a_][%w_]*)%s*=%s*.+$")
- end
- end
- local addition = false
- if not variable then
- local name = nameFormat(getPathDestination(sharedPath):gsub("%s", ""))
- local firstIndex = math.huge
- for index in pairs(lines) do
- if index < firstIndex then
- firstIndex = index
- end
- end
- doc:EditTextAsync(`local {name} = {sharedPath}\n`, firstIndex, 1, firstIndex, 1)
- variable = name
- addition = true
- end
- if addition then
- for index, line in pairs(lines) do
- doc:EditTextAsync(line:gsub(escapePattern(sharedPath), variable), index + 1, 1, index + 1, #line + 1)
- end
- else
- for index, line in pairs(lines) do
- doc:EditTextAsync(line:gsub(escapePattern(sharedPath), variable), index, 1, index, #line + 1)
- end
- end
- end
- end
- connections[#connections + 1] = ScriptEditorService.TextDocumentDidChange:Connect(function(doc: ScriptDocument, changes: {ScriptChange})
- for _, change in ipairs(changes) do
- local lineIndex = changes[#changes].range.start.line
- local line = doc:GetLine(lineIndex)
- if line:match("^%s*;;") then
- local selection = Selection:Get()
- if #selection == 1 then
- selection = selection[1]
- local src = doc:GetScript()
- if selection == src then return end
- if game:FindService(selection.ClassName) then
- if selection:IsA("Workspace") then return end
- local name = selection.ClassName
- doc:EditTextAsync(`local {name} = game:GetService("{name}")`, lineIndex, 1, lineIndex, #line + 1)
- else
- local lines = doc:GetText():split("\n")
- local path = getVariablePath(getRelativePath(src, selection), lines)
- if selection:IsA("ModuleScript") then
- doc:EditTextAsync(`local {nameFormat(selection.Name)} = require({path})`, lineIndex, 1, lineIndex, #line + 1)
- else
- doc:EditTextAsync(`local {nameFormat(selection.Name)} = {path}`, lineIndex, 1, lineIndex, #line + 1)
- end
- ensureService(doc, path:match('^([%a_][%w_]*)'))
- optimizePathReferences(doc)
- end
- end
- elseif line:match("^%s*local%s+%w[%w_]*%s*=%s*;;$") then
- local selection = Selection:Get()
- if #selection == 1 then
- selection = selection[1]
- local src = doc:GetScript()
- if selection == src then return end
- if game:FindService(selection.ClassName) then
- if selection:IsA("Workspace") then return end
- local name = selection.ClassName
- doc:EditTextAsync(`game:GetService("{name}")`, lineIndex, #line - 1, lineIndex, #line + 1)
- else
- local lines = doc:GetText():split("\n")
- local path = getVariablePath(getRelativePath(src, selection), lines)
- if selection:IsA("ModuleScript") then
- doc:EditTextAsync(`require({path})`, lineIndex, #line - 1, lineIndex, #line + 1)
- else
- doc:EditTextAsync(path, lineIndex, #line - 1, lineIndex, #line + 1)
- end
- ensureService(doc, path:match('^([%a_][%w_]*)'))
- optimizePathReferences(doc)
- end
- end
- else
- local text = change.text
- if text:match("^%s*local%s+%w[%w_]*%s*=%s*") then
- for _, item in ipairs(items) do
- if item.textEdit.newText == text then
- if item.module == nil then
- doc:EditTextAsync("", lineIndex, 1, lineIndex, #line + 1)
- ensureService(doc, item.serviceClass)
- else
- local path = item.path
- if path then
- ensureService(doc, path:match('^([%a_][%w_]*)'))
- optimizePathReferences(doc)
- end
- break
- end
- end
- end
- end
- end
- end
- end)
- for _, service in ipairs(services) do
- local item = {
- label = service,
- preselect = true,
- kind = Enum.CompletionItemKind.Module,
- tags = {Enum.CompletionItemTag.TypeCorrect},
- detail = `GetService("{service}")`,
- serviceClass = service,
- textEdit = {
- newText = "",
- replace = {
- start = {},
- ["end"] = {},
- },
- },
- }
- items[#items + 1] = item
- end
- plugin.Unloading:Connect(function()
- for _, connection in ipairs(connections) do
- connection:Disconnect()
- end
- for _, connections in ipairs(modules) do
- for _, connection in ipairs(connections) do
- connection:Disconnect()
- end
- end
- table.clear(modules)
- table.clear(items)
- ScriptEditorService:DeregisterAutocompleteCallback(AutocompleteName)
- end)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement