Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds syntax highlighting to the CLI #639

Merged
merged 5 commits into from
Jun 21, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 9 additions & 3 deletions cli/src/org/partiql/cli/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import org.partiql.lang.eval.ExprValueFactory
import org.partiql.lang.eval.TypingMode
import org.partiql.lang.syntax.SqlParser
import org.partiql.shell.Shell
import org.partiql.shell.Shell.ShellConfiguration
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
Expand Down Expand Up @@ -109,6 +110,8 @@ private val inputFormatOpt = optParser.acceptsAll(listOf("input-format", "if"),
private val wrapIonOpt = optParser.acceptsAll(listOf("wrap-ion", "w"), "wraps Ion input file values in a bag, requires the input format to be ION, requires the query option")
.availableIf(queryOpt)

private val monochromeOpt = optParser.acceptsAll(listOf("monochrome", "m"), "removes syntax highlighting for the REPL")

johnedquinn marked this conversation as resolved.
Show resolved Hide resolved
private val outputFileOpt = optParser.acceptsAll(listOf("output", "o"), "output file, requires the query option (default: stdout)")
.availableIf(queryOpt)
.withRequiredArg()
Expand All @@ -132,6 +135,8 @@ private val outputFormatOpt = optParser.acceptsAll(listOf("output-format", "of")
* * -e --environment: takes an environment file to load as the initial global environment
* * -p --permissive: run the query in permissive typing mode (returns MISSING rather than error for data type
* mismatches)
* * Interactive only:
* * -m --monochrome: removes syntax highlighting for the REPL
* * Non interactive only:
* * -q --query: PartiQL query
* * -i --input: input file
Expand Down Expand Up @@ -182,7 +187,7 @@ fun main(args: Array<String>) = try {
if (optionSet.has(queryOpt)) {
runCli(environment, optionSet, compilerPipeline)
} else {
runShell(environment, compilerPipeline)
runShell(environment, optionSet, compilerPipeline)
}
} catch (e: OptionException) {
System.err.println("${e.message}\n")
Expand All @@ -193,8 +198,9 @@ fun main(args: Array<String>) = try {
exitProcess(1)
}

private fun runShell(environment: Bindings<ExprValue>, compilerPipeline: CompilerPipeline) {
Shell(valueFactory, System.out, parser, compilerPipeline, environment).start()
private fun runShell(environment: Bindings<ExprValue>, optionSet: OptionSet, compilerPipeline: CompilerPipeline) {
val config = ShellConfiguration(isMonochrome = optionSet.has(monochromeOpt))
Shell(valueFactory, System.out, parser, compilerPipeline, environment, config).start()
}

private fun runCli(environment: Bindings<ExprValue>, optionSet: OptionSet, compilerPipeline: CompilerPipeline) {
Expand Down
13 changes: 12 additions & 1 deletion cli/src/org/partiql/shell/Shell.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class Shell(
private val parser: Parser,
private val compiler: CompilerPipeline,
private val initialGlobal: Bindings<ExprValue>,
private val config: ShellConfiguration = ShellConfiguration()
) {

private val homeDir: Path = Paths.get(System.getProperty("user.home"))
Expand Down Expand Up @@ -122,11 +123,15 @@ class Shell(
}

private fun run(exiting: AtomicBoolean) = TerminalBuilder.builder().build().use { terminal ->

val highlighter = when {
this.config.isMonochrome -> null
else -> ShellHighlighter()
}
val reader = LineReaderBuilder.builder()
.terminal(terminal)
.parser(ShellParser)
.completer(NullCompleter())
.highlighter(highlighter)
.expander(ShellExpander)
.variable(LineReader.HISTORY_FILE, homeDir.resolve(".partiql/.history"))
.variable(LineReader.SECONDARY_PROMPT_PATTERN, PROMPT_2)
Expand Down Expand Up @@ -261,6 +266,12 @@ class Shell(
}
}
}

/**
* A configuration class representing any configurations specified by the user
* @param isMonochrome specifies the removal of syntax highlighting
*/
class ShellConfiguration(val isMonochrome: Boolean = false)
johnedquinn marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
114 changes: 108 additions & 6 deletions cli/src/org/partiql/shell/ShellHighlighter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,134 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
* language governing permissions and limitations under the License.
*/

package org.partiql.shell

import org.jline.builtins.Nano.SyntaxHighlighter
import com.amazon.ion.IonString
import com.amazon.ion.system.IonSystemBuilder
import org.jline.reader.Highlighter
import org.jline.reader.LineReader
import org.jline.utils.AttributedString
import org.jline.utils.AttributedStringBuilder
import org.jline.utils.AttributedStyle
import org.partiql.lang.errors.Property
import org.partiql.lang.syntax.ParserException
import org.partiql.lang.syntax.SqlLexer
import org.partiql.lang.syntax.SqlParser
import org.partiql.lang.syntax.Token
import org.partiql.lang.syntax.TokenType
import java.io.PrintStream
import java.nio.file.Path
import java.util.regex.Pattern

private val SUCCESS: AttributedStyle = AttributedStyle.DEFAULT.foreground(AttributedStyle.GREEN)
private val ERROR: AttributedStyle = AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)
private val INFO: AttributedStyle = AttributedStyle.DEFAULT
private val WARN: AttributedStyle = AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)

class ShellHighlighter(private val syntaxFile: Path) : Highlighter {
johnedquinn marked this conversation as resolved.
Show resolved Hide resolved
internal class ShellHighlighter() : Highlighter {

private val ion = IonSystemBuilder.standard().build()
private val lexer = SqlLexer(ion)
private val parser = SqlParser(ion)

/**
* Returns a highlighted string by passing the [input] string through the [lexer] and [parser] to identify and
* highlight tokens
*/
override fun highlight(reader: LineReader, input: String): AttributedString {
if (input.isEmpty()) return AttributedString(input)

var builder = AttributedStringBuilder()
builder.append(AttributedString(input))

// Map Between Line Number and Index
val lineIndexesMap = mutableMapOf<Int, Int>()
var currentLine = 0
lineIndexesMap[currentLine] = -1
input.forEachIndexed { index, char ->
if (char == '\n') lineIndexesMap[++currentLine] = index
}
johnedquinn marked this conversation as resolved.
Show resolved Hide resolved

private val syntax = SyntaxHighlighter.build(syntaxFile, "PartiQL")
// Get Tokens
val tokens: List<Token>
try {
tokens = lexer.tokenize(input)
} catch (e: Exception) {
return AttributedString(input, AttributedStyle().foreground(AttributedStyle.RED))
}

override fun highlight(reader: LineReader, line: String): AttributedString = syntax.highlight(line)
// Replace Token Colors
tokens.forEach { builder = createAttributeStringBuilder(it, lineIndexesMap, builder, false) }
johnedquinn marked this conversation as resolved.
Show resolved Hide resolved

// Parse
try {
parser.parseAstStatement(input)
} catch (e: ParserException) {
val column =
e.errorContext[Property.COLUMN_NUMBER]?.longValue()?.toInt() ?: return builder.toAttributedString()
val lineNumber =
e.errorContext[Property.LINE_NUMBER]?.longValue()?.toInt() ?: return builder.toAttributedString()
val token = tokens.find {
it.span.column.toInt() == column && it.span.line.toInt() == lineNumber
} ?: return builder.toAttributedString()
builder = createAttributeStringBuilder(token, lineIndexesMap, builder, true)
}
return builder.toAttributedString()
}

override fun setErrorPattern(errorPattern: Pattern?) {}

override fun setErrorIndex(errorIndex: Int) {}

/**
* Based on the [token] type and location, this function will return a replica of the [stringBuilder] with the
* token's location having an updated color-scheme (style). If [isFailure] is passed, the style will always be RED.
*/
private fun createAttributeStringBuilder(
token: Token,
lineIndexes: Map<Int, Int>,
stringBuilder: AttributedStringBuilder,
isFailure: Boolean
): AttributedStringBuilder {
val a = AttributedStringBuilder()
val style = when (isFailure) {
true -> AttributedStyle().foreground(AttributedStyle.RED)
false -> getStyle(token)
}
val column = token.span.column.toInt()
val lineNumber = token.span.line.toInt() - 1
val length = token.span.length.toInt()
val index = lineIndexes[lineNumber]?.plus(column) ?: return stringBuilder
a.append(stringBuilder.subSequence(0, index))
a.append(stringBuilder.subSequence(index, index + length), style)
a.append(stringBuilder.subSequence(index + length, stringBuilder.length))
return a
}

/**
* Sets the color and thickness of the string based on the [token] type
*/
private fun getStyle(token: Token): AttributedStyle {
alancai98 marked this conversation as resolved.
Show resolved Hide resolved
var style = AttributedStyle()
val attrCode = when (token.type) {
TokenType.KEYWORD -> if (token.isDataType) AttributedStyle.GREEN else AttributedStyle.CYAN
TokenType.AS, TokenType.FOR, TokenType.ASC, TokenType.LAST, TokenType.DESC,
TokenType.BY, TokenType.FIRST -> AttributedStyle.CYAN
TokenType.LITERAL -> if (token.value is IonString) AttributedStyle.YELLOW else AttributedStyle.BLUE
TokenType.ION_LITERAL -> AttributedStyle.YELLOW
TokenType.OPERATOR -> AttributedStyle.WHITE
TokenType.QUOTED_IDENTIFIER -> AttributedStyle.BRIGHT
TokenType.IDENTIFIER -> AttributedStyle.BRIGHT
TokenType.MISSING -> AttributedStyle.BLUE
TokenType.NULL -> AttributedStyle.BLUE
else -> AttributedStyle.WHITE
}
style = style.foreground(attrCode)

return when (token.type) {
TokenType.IDENTIFIER, TokenType.OPERATOR -> style.bold()
else -> style
}
}
johnedquinn marked this conversation as resolved.
Show resolved Hide resolved
}

private fun ansi(string: String, style: AttributedStyle) = AttributedString(string, style).toAnsi()
Expand Down
3 changes: 2 additions & 1 deletion docs/user/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Option Description
-i, --input <File> input file, requires the query option (optional)
-if, --input-format <InputFormat> input format, requires the query option (default: ION) [ION, PARTIQL]
-w, --wrap-ion wraps Ion input file values in a bag, requires the input format to be ION, requires the query option
-m, --monochrome removes syntax highlighting for the REPL
-o, --output <File> output file, requires the query option (default: stdout)
-of, --output-format <OutputFormat> output format, requires the query option (default: PARTIQL) [PARTIQL, PARTIQL_PRETTY, ION_TEXT, ION_BINARY]
-p, --permissive run the PartiQL query in PERMISSIVE typing mode
Expand Down Expand Up @@ -61,7 +62,7 @@ To start an interactive read, eval, print loop (REPL) execute:
> Note that running directly with Gradle will eat arrow keys and control sequences due to the Gradle daemon.

```shell
./cli/shell
./cli/shell.sh
```

You will see a prompt that looks as follows:
Expand Down
7 changes: 7 additions & 0 deletions lang/src/org/partiql/lang/syntax/Token.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,11 @@ data class Token(
isBinaryOperator || isSpecialOperator -> OPERATOR_PRECEDENCE[text] ?: 0
else -> 0
}

val isDataType: Boolean
get() = when {
type == TokenType.KEYWORD && CORE_TYPE_NAME_ARITY_MAP.keys.union(TYPE_ALIASES.keys)
.contains(text?.toLowerCase()) -> true
else -> false
}
}