Skip to content

Instantly share code, notes, and snippets.

@EricRabil
Created August 13, 2021 00:16
Show Gist options
  • Save EricRabil/d182ee3473700116f8ee744c84b25322 to your computer and use it in GitHub Desktop.
Save EricRabil/d182ee3473700116f8ee744c84b25322 to your computer and use it in GitHub Desktop.
Completion suggestions for JavaScriptCore JSContext
//
// JSContext+Completion.swift
// BarcelonaJS
//
// Created by Eric Rabil on 8/12/21.
// Copyright © 2021 Eric Rabil. All rights reserved.
//
// Shamelessly ported from https://github.com/nodejs/node/blob/master/lib/repl.js
//
import Foundation
import JavaScriptCore
private extension Collection where Element == [String] {
func uniqueCompletions(withFilter filter: String?) -> [String] {
var completions = [String]()
var uniqueSet = Set<String>()
for group in self {
var group = group
if let filter = filter {
group = group.filter { entry in
entry.starts(with: filter)
}
}
group.sorted(by: >).forEach { entry in
if !uniqueSet.contains(entry) {
completions.insert(entry, at: 0)
uniqueSet.insert(entry)
}
}
}
return completions
}
}
private extension String {
func match(_ expression: String) -> String? {
guard let range = range(of: expression, options: .regularExpression) else {
return nil
}
return String(self[range])
}
}
public extension JSContext {
func completion(forLine line: String) -> [String] {
let match = line.match(#"(?:[a-zA-Z_$](?:\w|\$)*\??\.)*[a-zA-Z_$](?:\w|\$)*\??\.?$"#) ?? ""
if line.count > 0, match.count == 0 {
return []
}
return completionGroups(forExpression: match)
}
}
private extension JSContext {
func topLevelCompletions() -> [[String]] {
var completionGroups = [[String]]()
var proto = globalObject.prototype
while proto != nil {
completionGroups.append(proto!.propertyNames)
proto = proto!.prototype
}
completionGroups.append(globalObject.propertyNames)
return completionGroups
}
func completionGroups(forExpression expression: String) -> [String] {
var expression = expression
var refinement: String? = nil
if expression.hasSuffix(".") {
expression = String(expression.prefix(expression.count - 1))
} else if expression.count > 0 {
var bits = expression.split(separator: ".")
refinement = String(bits.popLast()!)
expression = bits.joined(separator: ".")
}
if expression.count == 0 {
return topLevelCompletions().uniqueCompletions(withFilter: refinement)
}
var chaining = "."
if expression.hasSuffix("?") {
expression = String(expression.prefix(expression.count - 1))
chaining = "?."
}
let evalExpr = "try { \(expression) } catch {}"
guard let obj = evaluateScript(evalExpr) else {
return []
}
var proto: JSValue? = nil
var memberGroups: [[String]] = []
if (obj.isObject && !obj.isNull()) || obj.isFunction {
memberGroups.append(obj.propertyNames)
proto = obj.prototype
} else {
proto = obj["constructor"]?["prototype"]
}
var sentinel = 5
while proto != nil, sentinel != 0 {
memberGroups.append(proto!.propertyNames)
proto = proto!.prototype
sentinel -= 1
}
var completionGroups = [[String]]()
if memberGroups.count > 0 {
expression += chaining
memberGroups.forEach { group in
completionGroups.append(group.map { member in "\(expression)\(member)" })
}
}
if let refinement = refinement {
return completionGroups.uniqueCompletions(withFilter: expression + refinement)
} else {
return completionGroups.uniqueCompletions(withFilter: nil)
}
}
}
import Foundation
import JavaScriptCore
let context = JSContext()
let ln = LineNoise()
ln.setCompletionCallback { text in
context.completion(forLine: text)
}
while let code = try? ln.getLine(prompt: ">>> ") {
ln.addHistory(code)
print("\r\n" + context.evaluateScript(code)!.description.replacingOccurrences(of: "\n", with: "\r\n"), terminator: "\r\n")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment