Skip to content

Commit

Permalink
Editor Error Markers (#3087)
Browse files Browse the repository at this point in the history
  • Loading branch information
jameskerr authored Jun 12, 2024
1 parent a9af42a commit c4ff726
Show file tree
Hide file tree
Showing 22 changed files with 286 additions and 80 deletions.
3 changes: 2 additions & 1 deletion apps/zui/src/app/core/models/active-query.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {QueryModel} from "src/js/models/query-model"
import {QueryVersion} from "src/js/state/QueryVersions/types"
import {EditorSnapshot} from "src/models/editor-snapshot"

export class ActiveQuery {
constructor(
Expand Down Expand Up @@ -60,6 +61,6 @@ export class ActiveQuery {
}

toZed() {
return QueryModel.versionToZed(this.version)
return new EditorSnapshot(this.version).toQueryText()
}
}
17 changes: 17 additions & 0 deletions apps/zui/src/components/zed-editor-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export class ZedEditorHandler {
constructor(public monaco, public editor) {}

focus() {
if (!this.mounted) return
setTimeout(() => this.editor.focus())
}

setErrors(markers) {
if (!this.mounted) return
this.monaco.editor.setModelMarkers(this.editor.getModel(), "zed", markers)
}

private get mounted() {
return !!this.monaco && !!this.editor
}
}
41 changes: 20 additions & 21 deletions apps/zui/src/components/zed-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {Editor} from "@monaco-editor/react"
import {useEffect, useRef} from "react"
import {Editor, useMonaco} from "@monaco-editor/react"
import {useEffect, useMemo, useRef} from "react"
import {useSelector} from "react-redux"
import {cmdOrCtrl} from "src/app/core/utils/keyboard"
import Config from "src/js/state/Config"
import {Marker} from "src/js/state/Editor/types"
import {useColorScheme} from "src/util/hooks/use-color-scheme"
import {ZedEditorHandler} from "./zed-editor-handler"

/**
*
Expand All @@ -30,31 +32,32 @@ export function ZedEditor(props: {
value: string
onChange: (value: string | undefined, ev: any) => void
autoFocus?: boolean
markers?: Marker[]
}) {
const ref = useRef<any>()
const {isDark} = useColorScheme()
const monaco = useMonaco()
const handler = useMemo(
() => new ZedEditorHandler(monaco, ref.current),
[monaco, ref.current]
)

// Keep this thing in focus as much as possible.
// Probably want to move this into parent.
useEffect(() => {
setTimeout(() => {
if (ref.current) {
ref.current.focus()
}
})
}, [props.path, props.value])
useEffect(() => handler.focus(), [props.path, props.value, handler])
useEffect(() => handler.setErrors(props.markers), [props.markers, handler])

return (
<Editor
wrapperProps={{
"data-testid": props.testId,
}}
height="100%"
width="100%"
value={props.value}
onChange={props.onChange}
language="zed"
onChange={props.onChange}
onMount={(editor) => (ref.current = editor)}
path={props.path}
theme={isDark ? "vs-dark" : "vs-light"}
value={props.value}
width="100%"
wrapperProps={{
"data-testid": props.testId,
}}
options={{
minimap: {enabled: false},
renderLineHighlight: "none",
Expand All @@ -64,10 +67,6 @@ export function ZedEditor(props: {
fontVariations: "inherit",
lineNumbersMinChars: 4,
}}
onMount={(editor) => {
ref.current = editor
}}
path={props.path}
/>
)
}
2 changes: 1 addition & 1 deletion apps/zui/src/domain/editor/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const describe = createOperation(
async (ctx, string, pool?) => {
try {
const resp = await lake.client.describeQuery(string, pool)
return resp.toJS()
return resp.error ? {error: resp} : resp
} catch (error) {
return {error: error.toString()}
}
Expand Down
26 changes: 1 addition & 25 deletions apps/zui/src/js/models/query-model.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import {Query} from "src/js/state/Queries/types"
import {isEmpty, last} from "lodash"
import {QueryPinInterface} from "../state/Editor/types"
import buildPin from "src/js/state/Editor/models/build-pin"
import {last} from "lodash"
import {QueryVersion} from "src/js/state/QueryVersions/types"
import {QuerySource} from "src/js/api/queries/types"

Expand Down Expand Up @@ -59,26 +57,4 @@ export class QueryModel implements Query {
isReadOnly: this.isReadOnly,
}
}

static versionToZed(version: QueryVersion): string {
let pinS = []
if (!isEmpty(version?.pins))
pinS = version.pins
.filter((p) => !p.disabled)
.map<QueryPinInterface>(buildPin)
.map((p) => p.toZed())
let s = pinS
.concat(version?.value ?? "")
.filter((s) => s.trim() !== "")
.join(" | ")
.trim()

if (isEmpty(s)) s = "*"

return s
}

toString(): string {
return QueryModel.versionToZed(this.current)
}
}
3 changes: 2 additions & 1 deletion apps/zui/src/js/state/Current/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import lake from "src/js/models/lake"
import {defaultLake} from "src/js/initializers/initLakeParams"
import {getActive} from "../Tabs/selectors"
import QueryInfo from "../QueryInfo"
import {EditorSnapshot} from "src/models/editor-snapshot"

export const getHistory = (
state,
Expand Down Expand Up @@ -54,7 +55,7 @@ export const getVersion = (state: State): QueryVersion => {
}

export const getQueryText = createSelector(getVersion, (version) => {
return QueryModel.versionToZed(version)
return new EditorSnapshot(version).toQueryText()
})

const getRawSession = (state: State) => {
Expand Down
6 changes: 5 additions & 1 deletion apps/zui/src/js/state/Editor/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {createSlice, PayloadAction} from "@reduxjs/toolkit"
import buildPin from "./models/build-pin"
import {FromQueryPin, QueryPin, TimeRangeQueryPin} from "./types"
import {FromQueryPin, QueryPin, TimeRangeQueryPin, Marker} from "./types"

const slice = createSlice({
name: "TAB_EDITOR",
Expand All @@ -9,6 +9,7 @@ const slice = createSlice({
pins: [] as QueryPin[],
pinEditIndex: null as null | number,
pinHoverIndex: null as null | number,
markers: [] as Marker[],
},
reducers: {
setValue(s, a: PayloadAction<string>) {
Expand Down Expand Up @@ -113,6 +114,9 @@ const slice = createSlice({
})
}
},
setMarkers(s, a: PayloadAction<Marker[]>) {
s.markers = a.payload
},
},
})

Expand Down
4 changes: 4 additions & 0 deletions apps/zui/src/js/state/Editor/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ export const getSnapshot = activeTabSelect((tab) => {
export const isEmpty = createSelector(getValue, getPins, (value, pins) => {
return value.trim() === "" && pins.length === 0
})

export const getMarkers = activeTabSelect((tab) => {
return tab.editor.markers
})
8 changes: 8 additions & 0 deletions apps/zui/src/js/state/Editor/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,11 @@ export interface QueryPinInterface {
empty(): boolean
toZed(): string
}

export type Marker = {
message: string
startLineNumber: number
startColumn: number
endLineNumber: number
endColumn: number
}
2 changes: 1 addition & 1 deletion apps/zui/src/js/state/QueryInfo/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const get = activeTabSelect((tab) => {
return tab.queryInfo
})

export const getParseError = createSelector(get, (info) => info.error)
export const getParseError = createSelector(get, (info) => info.error?.error)
export const getIsParsed = createSelector(get, (info) => info.isParsed)
export const getPoolName = createSelector(get, (info) => {
let source = find(info.sources || [], {kind: "Pool"})
Expand Down
2 changes: 1 addition & 1 deletion apps/zui/src/models/active.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import {Session} from "./session"
import Current from "src/js/state/Current"
import {EditorSnapshot} from "./editor-snapshot"
import {BrowserTab} from "./browser-tab"
import Editor from "src/js/state/Editor"
import {Frame} from "./frame"
import {getActiveTab} from "src/js/state/Tabs/selectors"
import Editor from "src/js/state/Editor"

export class Active extends DomainModel {
static get tab() {
Expand Down
32 changes: 31 additions & 1 deletion apps/zui/src/models/editor-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import {nanoid} from "@reduxjs/toolkit"
import {isEqual} from "lodash"
import {queryPath} from "src/app/router/utils/paths"
import {DomainModel} from "src/core/domain-model"
import {QueryPin} from "src/js/state/Editor/types"
import buildPin from "src/js/state/Editor/models/build-pin"
import {QueryPin, QueryPinInterface} from "src/js/state/Editor/types"
import QueryVersions from "src/js/state/QueryVersions"
import {SourceSet} from "./editor-snapshot/source-set"
import {Validator} from "./editor-snapshot/validator"

type Attrs = {
version: string
Expand All @@ -14,6 +17,8 @@ type Attrs = {
}

export class EditorSnapshot extends DomainModel<Attrs> {
validator = new Validator()

constructor(attrs: Partial<Attrs> = {}) {
super({
version: nanoid(),
Expand Down Expand Up @@ -50,6 +55,23 @@ export class EditorSnapshot extends DomainModel<Attrs> {
return this.attrs.parentId
}

activePins() {
return this.attrs.pins
.filter((pin) => !pin.disabled)
.map<QueryPinInterface>((attrs) => buildPin(attrs))
}

toSourceSet() {
return new SourceSet(
this.activePins().map((pin) => pin.toZed()),
this.attrs.value
)
}

toQueryText() {
return this.toSourceSet().contents
}

equals(other: EditorSnapshot) {
return (
isEqual(this.attrs.pins, other.attrs.pins) &&
Expand All @@ -65,4 +87,12 @@ export class EditorSnapshot extends DomainModel<Attrs> {
clone(attrs: Partial<Attrs>) {
return new EditorSnapshot({...this.attrs, ...attrs})
}

async isValid() {
return this.validator.validate(this)
}

get errors() {
return this.validator.errors
}
}
37 changes: 37 additions & 0 deletions apps/zui/src/models/editor-snapshot/compilation-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {Marker} from "src/js/state/Editor/types"
import {Source} from "./source"
import {SourceSet} from "./source-set"

export class CompilationError {
constructor(
public message: string,
public offset: number,
public end: number
) {}

toMarker(sourceSet: SourceSet) {
const source = sourceSet.sourceOf(this.offset)
const start = this.getStartPosition(source)
const end = this.getEndPosition(source)

return {
message: this.message,
startLineNumber: start.lineNumber,
startColumn: start.column,
endLineNumber: end ? end.lineNumber : start.lineNumber,
endColumn: end ? end.column : start.column + 1,
} as Marker
}

private hasRange() {
return this.end >= 0
}

private getStartPosition(source: Source) {
return source.position(this.offset)
}

private getEndPosition(source: Source) {
return this.hasRange() ? source.position(this.end) : null
}
}
28 changes: 28 additions & 0 deletions apps/zui/src/models/editor-snapshot/source-set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {Source} from "./source"

export class SourceSet {
sources: Source[] = []
contents: string = ""

constructor(prefixes: string[], mainText: string) {
prefixes
.concat(mainText)
.filter((snippet) => snippet.trim().length)
.forEach((snippet) => this.appendSource(snippet))

if (!this.sources.length) this.appendSource("*")
}

appendSource(text: string) {
const isFirst = this.contents.length === 0
if (!isFirst) this.contents += " | "
this.sources.push(new Source(this.contents.length, text))
this.contents += text
}

sourceOf(pos: number) {
return this.sources.find(
(s) => pos >= s.start && pos < s.start + s.length + 1
)
}
}
33 changes: 33 additions & 0 deletions apps/zui/src/models/editor-snapshot/source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {sortedIndex} from "lodash"

export type Position = {
column: number
lineNumber: number
}

export class Source {
start: number
length: number
lines: number[]

constructor(start: number, text: string) {
this.start = start
this.length = text.length
this.lines = [0]

for (let k = 0; k < text.length; k++) {
if (text[k] === "\n") {
this.lines.push(k + 1)
}
}
}

position(pos: number): Position {
let offset = pos - this.start
let i = sortedIndex(this.lines, offset) - 1
return {
column: offset - this.lines[i] + 1,
lineNumber: i + 1,
}
}
}
Loading

0 comments on commit c4ff726

Please sign in to comment.