Skip to content

Commit

Permalink
Create a new Completion Provider
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnnyMorganz committed Jun 28, 2020
1 parent 0d00f8a commit d67fea7
Show file tree
Hide file tree
Showing 5 changed files with 322 additions and 352 deletions.
315 changes: 315 additions & 0 deletions src/completionProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
// Roblox Completion Provider
// Provides completion items for Classes and DataTypes, as well as properties with custom DataTypes

import * as vscode from "vscode"
import { getAutocompleteDump } from "./autocompleteDump"
import { ApiClass, ApiMember, ApiPropertySecurity, getApiDump, UNCREATABLE_TAGS } from "./dump"
import { createDocumentationString, createParameterLabel, createStructParameterLabel, inferType } from "./utils"

const UNSCRIPTABLE_TAGS: Set<string> = new Set([
"Deprecated",
"Hidden",
"NotBrowsable",
"NotScriptable",
])

const IMPORT_PATTERN = /^local \w+ = game:GetService\("\w+"\)\s*$/

const getAllowedClassMembers = (member: ApiMember) => {
const tags = member.Tags
if (tags !== undefined) {
for (const tag of tags) {
if (UNSCRIPTABLE_TAGS.has(tag)) {
return false
}
}
}
return true
}

const getClassCompletionItemKind = (type: "Function" | "Callback" | "Event" | "Property") => {
switch (type) {
case "Function":
return vscode.CompletionItemKind.Method
case "Property":
return vscode.CompletionItemKind.Field
case "Callback":
return vscode.CompletionItemKind.Constructor
case "Event":
return vscode.CompletionItemKind.Event
}
}

const getClassCompletionItemDetail = (member: ApiMember, service: ApiClass) => {
switch (member.MemberType) {
case "Function":
return `(method) ${service.Name}:${member.Name}(${member.Parameters.map(parameter => createParameterLabel(parameter)).join(", ")}): ${member.ReturnType ? member.ReturnType.Name : "unknown"}`
case "Property":
return `(property) ${service.Name}.${member.Name}: ${member.ValueType ? member.ValueType.Name : "unknown"}`
case "Callback":
return `(callback) ${service.Name}.${member.Name} = function (${member.Parameters.map(parameter => `${parameter.Name}: ${parameter.Type ? parameter.Type.Name : "unknown"}`).join(", ")})`
case "Event":
return `(event) ${service.Name}.${member.Name}(${member.Parameters.map(parameter => `${parameter.Name}: ${parameter.Type ? parameter.Type.Name : "unknown"}`).join(", ")})`
}
}

export class RobloxCompletionProvider implements vscode.CompletionItemProvider {
private classCompletion: Promise<Array<Map<string, vscode.CompletionItem[]>>>
private structCompletion: Promise<Array<Map<string, vscode.CompletionItem[]>>>
private itemStructNames: Promise<vscode.CompletionItem[]>

constructor() {
this.classCompletion = (async () => {
const dotCompletion = new Map<string, vscode.CompletionItem[]>()
const colonCompletion = new Map<string, vscode.CompletionItem[]>()

const classes = (await getApiDump()).Classes
for (const klass of classes) {
const dotOperatorItems: vscode.CompletionItem[] = []
const colonOperatorItems: vscode.CompletionItem[] = []

for (const member of klass.Members.filter(getAllowedClassMembers)) {
if (member.MemberType === "Property") {
const security = member.Security as ApiPropertySecurity
if (security.Read !== "None" && security.Write !== "None") {
continue
}
} else if (member.Security !== "None") {
continue
}

const completionItem = new vscode.CompletionItem(
member.Name,
getClassCompletionItemKind(member.MemberType)
)
completionItem.detail = getClassCompletionItemDetail(member, klass)
completionItem.documentation = createDocumentationString(member, member.MemberType, klass.Name)

if (member.MemberType === "Function") {
colonOperatorItems.push(completionItem)
} else {
dotOperatorItems.push(completionItem)
}
}

dotCompletion.set(klass.Name, dotOperatorItems)
colonCompletion.set(klass.Name, colonOperatorItems)
}

return [ dotCompletion, colonCompletion ]
})()

this.structCompletion = (async () => {
const dotCompletion = new Map<string, vscode.CompletionItem[]>()
const colonCompletion = new Map<string, vscode.CompletionItem[]>()

const autocompleteDump = await getAutocompleteDump()

for (const itemStruct of autocompleteDump.ItemStruct) {
const dotOperatorItems: vscode.CompletionItem[] = []
const colonOperatorItems: vscode.CompletionItem[] = []

itemStruct.properties.map(property => {
const item = new vscode.CompletionItem(
property.name,
vscode.CompletionItemKind.Field,
)
item.detail = `(property) ${itemStruct.name}.${property.name}: ${property.type}`
item.documentation = new vscode.MarkdownString(`${property.description ? property.description + "\n\n" : ""}[Developer Reference](https://developer.roblox.com/en-us/api-reference/datatype/${itemStruct.name})`)
return item
}).forEach(item => dotOperatorItems.push(item))

const completedFuncs: { [name: string]: boolean } = {}

itemStruct.functions.map(func => {
const item = new vscode.CompletionItem(
func.name,
func.static ? vscode.CompletionItemKind.Function : vscode.CompletionItemKind.Method,
)
item.detail = `(${func.static ? "function" : "method"}) ${itemStruct.name}.${func.name}(${func.parameters.map(parameter => createStructParameterLabel(parameter)).join(", ")}): ${func.returns.length > 0 ? func.returns.map((ret) => ret.type).join(", ") : "unknown"}`
item.documentation = new vscode.MarkdownString(`${func.description ? func.description + "\n\n" : ""}[Developer Reference](https://developer.roblox.com/en-us/api-reference/datatype/${itemStruct.name})`)
return { item, func }
}).forEach(
({ item, func }) => {
// Merge together overloaded functions
if (completedFuncs[func.name] === undefined) {
const count = itemStruct.functions.filter(f => f.name === func.name).length
if (count > 1) {
item.detail += ` (+${count - 1} overload${count === 1 ? "" : "s"})`
}

func.static ? dotOperatorItems.push(item) : colonOperatorItems.push(item)
completedFuncs[func.name] = true
}
})

dotCompletion.set(itemStruct.name, dotOperatorItems)
colonCompletion.set(itemStruct.name, colonOperatorItems)
}

return [ dotCompletion, colonCompletion ]
})()

this.itemStructNames = (async () => {
const autocompleteDump = await getAutocompleteDump()
return autocompleteDump.ItemStruct.filter(
(itemStruct) => {
return itemStruct.functions.filter(
func => func.static,
).length > 0 || itemStruct.properties.filter((property) => property.static).length > 0
},
).map(
(itemStruct) => new vscode.CompletionItem(itemStruct.name, vscode.CompletionItemKind.Class),
)
})()
}

public async provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken,
context: vscode.CompletionContext,
) {
const codeAtLine = document.lineAt(position.line).text.substr(0, position.character)
const codeString = codeAtLine.match(/([\w.:()'"]+)([.:])([\w()"']*)$/)

console.log(codeAtLine, codeString)

if (codeString !== null) {
const indexString = codeString[1]
const operator = codeString[2]
const extra = codeString[3]

const types = await inferType(document, position, indexString)

const mainType = types[types.length - 1]
if (mainType === undefined) {
return null
}

if (mainType.Category === "Class") {
const apiDump = await getApiDump()
const service = apiDump.Classes.find(klass => klass.Name === mainType.Name)
if (service !== undefined && service.Tags !== undefined && service.Tags.includes("Service")) {
// Provide auto import if it is a service and is not already present
const documentText = document.getText()

if (!documentText.match(new RegExp(`^local ${service.Name}\\s*=\\s*`, "m"))) {
const insertText = `local ${service.Name} = game:GetService("${service.Name}")\n`
const lines = documentText.split(/\n\r?/)

const firstImport = lines.findIndex(line => line.match(IMPORT_PATTERN))
let lineNumber = Math.max(firstImport, 0)

while (lineNumber < lines.length) {
if (
!lines[lineNumber].match(IMPORT_PATTERN)
|| lines[lineNumber] > insertText
) {
break
}
lineNumber++
}

const item = new vscode.CompletionItem(
service.Name,
vscode.CompletionItemKind.Class,
)

item.additionalTextEdits = [
vscode.TextEdit.insert(
new vscode.Position(lineNumber, 0),
insertText + (firstImport === -1 ? "\n" : ""),
),
]

if (operator !== "") {
item.command = { command: "editor.action.triggerSuggest", title: "Re-trigger completions" }
}

item.detail = `Auto-import ${service.Name}`
item.documentation = new vscode.MarkdownString(`${service.Description ? service.Description + "\n\n" : ""}[Developer Reference](https://developer.roblox.com/en-us/api-reference/class/${service.Name})`)
item.insertText = operator ? "" : service.Name
item.preselect = true

return [item]
}
}

const [ dotCompletion, colonCompletion ] = await this.classCompletion
if (operator === ".") {
return dotCompletion.get(mainType.Name)
} else if (operator === ":") {
return colonCompletion.get(mainType.Name)
}
} else if (mainType.Category === "DataType") {
const [ dotCompletion, colonCompletion ] = await this.structCompletion

// Provide support for Instance.new() etc.
if (extra !== null) {
const extraMatch = extra.match(/([\w.:]+)\(([\w"',\s.:()-]*)?$/)
if (extraMatch !== null) {
const functionName = extraMatch[1]
const parameters = extraMatch[2]

const itemStruct = (await getAutocompleteDump()).ItemStruct.find(struct => struct.name === mainType.Name)
const itemStructFunc = itemStruct?.functions.find(func => func.name === functionName)
const parameter = itemStructFunc?.parameters[parameters.split(",").length - 1]

if (parameter !== undefined && parameter.constraint !== undefined) {
const constraintSplit = parameter.constraint.split(":")
const objectType = constraintSplit[0]
const constraint = constraintSplit[1] || "any"

const apiDump = await getApiDump()
const options = apiDump.Classes.filter(klass => {
if (objectType === "Instance") {
if (constraint === "any") {
return true
} else if (constraint === "isScriptCreatable") {
const tags = klass.Tags
if (tags) {
for (const tag of tags) {
if (UNCREATABLE_TAGS.has(tag)) {
return false
}
}
}
return true
}
}
return false
}).map(klass => {
const completionItem = new vscode.CompletionItem(
klass.Name,
vscode.CompletionItemKind.Constant,
)

completionItem.documentation = new vscode.MarkdownString(`${klass.Description ? klass.Description + "\n\n" : ""}[Developer Reference](https://developer.roblox.com/en-us/api-reference/class/${klass.Name})`)
return completionItem
})

return options
}
return null
}
}

if (operator === ".") {
if (mainType.Static === true) {
return dotCompletion.get(mainType.Name)?.filter(item => item.kind === vscode.CompletionItemKind.Function)
} else {
return dotCompletion.get(mainType.Name)?.filter(item => item.kind !== vscode.CompletionItemKind.Function)
}
} else if (operator === ":" && mainType.Static === false) {
return colonCompletion.get(mainType.Name)
}
}

return null
} else {
// Provide completion items for creatable DataTypes
return this.itemStructNames
}
}
}
7 changes: 2 additions & 5 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@
import * as vscode from "vscode"
import { RobloxColorProvider } from "./color"
import { Companion } from "./companion"
import { RobloxCompletionProvider } from "./completionProvider"
import { EnumCompletionProvider } from "./enum"
import { ItemStructCompletionProvider } from "./itemStruct"
import { LuaLibraryCompletionProvider } from "./luaLibrary"
import { RojoHandler } from "./rojo"
import { ServiceCompletionProvider } from "./services"
import { RobloxSignatureProvider } from "./signatureProvider"
import { inferType } from "./utils"
const SELECTOR = { scheme: "file", language: "lua" }

export async function activate(context: vscode.ExtensionContext) {
Expand Down Expand Up @@ -42,8 +40,7 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.languages.registerColorProvider(SELECTOR, new RobloxColorProvider()))

context.subscriptions.push(vscode.languages.registerCompletionItemProvider(SELECTOR, new EnumCompletionProvider(), "."))
context.subscriptions.push(vscode.languages.registerCompletionItemProvider(SELECTOR, new ServiceCompletionProvider(), ".", ":"))
context.subscriptions.push(vscode.languages.registerCompletionItemProvider(SELECTOR, new LuaLibraryCompletionProvider(), "."))
context.subscriptions.push(vscode.languages.registerCompletionItemProvider(SELECTOR, new ItemStructCompletionProvider(), "."))
context.subscriptions.push(vscode.languages.registerSignatureHelpProvider(SELECTOR, new RobloxSignatureProvider(), "(", ","))
context.subscriptions.push(vscode.languages.registerCompletionItemProvider(SELECTOR, new RobloxCompletionProvider(), ".", ":", "\"", "'"))
}
Loading

0 comments on commit d67fea7

Please sign in to comment.