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

[wip] refactor playground editor #76

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 45 additions & 155 deletions play/src/codemirror/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ import {
showTooltip,
gutter,
GutterMarker,
PanelConstructor,
Panel,
showPanel,
getPanel,
} from '@codemirror/view'
import {
Text,
Expand All @@ -32,37 +28,28 @@ import elt from 'crelt'

type Severity = 'hint' | 'info' | 'warning' | 'error'

export interface Label {
message: string
export interface Span {
sourceId: string
from: number
to: number
}

export interface Label {
message: string
span: Span
}

export interface Note {
message: string
}

export interface Report {
message: string
from: number
to: number
sourceId: string
span: Span
severity: Severity
markClass?: string
// labels: Array<Label>
// notes: Array<Note>
renderMessage?: () => Node
actions?: readonly Action[]
}

/// An action associated with a diagnostic.
export interface Action {
/// The label to show to the user. Should be relatively short.
name: string
/// The function to call when the user activates this action. Is
/// given the diagnostic's _current_ position, which may have
/// changed since the creation of the diagnostic, due to editing.
apply: (view: EditorView, from: number, to: number) => void
labels: Array<Label>
notes: Array<Note>
}

type ReportFilter = (reports: readonly Report[]) => Report[]
Expand All @@ -79,30 +66,34 @@ interface DiagnosticGutterConfig {
}

class SelectedReport {
constructor(readonly from: number, readonly to: number, readonly report: Report) {}
constructor(
readonly from: number,
readonly to: number,
readonly report: Report,
) {}
}

class DiagnosticState {
constructor(
readonly reports: DecorationSet,
readonly panel: PanelConstructor | null,
readonly selected: SelectedReport | null,
) {}

static init(reports: readonly Report[], panel: PanelConstructor | null, state: EditorState) {
static init(reports: readonly Report[], state: EditorState) {
// Filter the list of reports for which to create markers
let markedReports = reports
let reportFilter = state.facet(diagnosticConfig).markerFilter
if (reportFilter) markedReports = reportFilter(markedReports)

let ranges = Decoration.set(
markedReports.map((d: Report) => {
const s = d.span
// For zero-length ranges or ranges covering only a line break, create a widget
return d.from == d.to || (d.from == d.to - 1 && state.doc.lineAt(d.from).to == d.from)
return s.from == s.to || (s.from == s.to - 1 && state.doc.lineAt(s.from).to == s.from)
? Decoration.widget({
widget: new ReportWidget(d),
report: d,
}).range(d.from)
}).range(s.from)
: Decoration.mark({
attributes: {
class:
Expand All @@ -111,11 +102,11 @@ class DiagnosticState {
(d.markClass ? ' ' + d.markClass : ''),
},
report: d,
}).range(d.from, d.to)
}).range(s.from, s.to)
}),
true,
)
return new DiagnosticState(ranges, panel, findReport(ranges))
return new DiagnosticState(ranges, findReport(ranges))
}
}

Expand All @@ -140,32 +131,21 @@ function hideTooltip(tr: Transaction, tooltip: Tooltip) {
)
}

function maybeEnableDiagnostic(state: EditorState, effects: readonly StateEffect<unknown>[]) {
return state.field(diagnosticState, false)
? effects
: effects.concat(StateEffect.appendConfig.of(diagnosticExtensions))
}

/// Returns a transaction spec which updates the current set of
/// reports, and enables the diagnostic extension if if wasn't already
/// active.
export function setReports(state: EditorState, reports: readonly Report[]): TransactionSpec {
export function setReports(reports: readonly Report[]): TransactionSpec {
return {
effects: maybeEnableDiagnostic(state, [setReportsEffect.of(reports)]),
effects: setReportsEffect.of(reports),
}
}

/// The state effect that updates the set of active reports. Can
/// be useful when writing an extension that needs to track these.
export const setReportsEffect = StateEffect.define<readonly Report[]>()

const togglePanel = StateEffect.define<boolean>()

const movePanelSelection = StateEffect.define<SelectedReport>()

const diagnosticState = StateField.define<DiagnosticState>({
create() {
return new DiagnosticState(Decoration.none, DiagnosticPanel.open, null)
return new DiagnosticState(Decoration.none, null)
},
update(value, tr) {
if (tr.docChanged) {
Expand All @@ -176,37 +156,22 @@ const diagnosticState = StateField.define<DiagnosticState>({
selected =
findReport(mapped, value.selected.report, selPos) || findReport(mapped, null, selPos)
}
value = new DiagnosticState(mapped, value.panel, selected)
value = new DiagnosticState(mapped, selected)
}

for (let effect of tr.effects) {
if (effect.is(setReportsEffect)) {
value = DiagnosticState.init(effect.value, value.panel, tr.state)
} else if (effect.is(togglePanel)) {
value = new DiagnosticState(
value.reports,
effect.value ? DiagnosticPanel.open : null,
value.selected,
)
value = DiagnosticState.init(effect.value, tr.state)
} else if (effect.is(movePanelSelection)) {
value = new DiagnosticState(value.reports, value.panel, effect.value)
value = new DiagnosticState(value.reports, effect.value)
}
}

return value
},
provide: (f) => [
showPanel.from(f, (val) => val.panel),
EditorView.decorations.from(f, (s) => s.reports),
],
provide: (f) => [EditorView.decorations.from(f, (s) => s.reports)],
})

/// Returns the number of active diagnostic reports in the given state.
export function reportCount(state: EditorState) {
const diagnostic_ = state.field(diagnosticState, false)
return diagnostic_ ? diagnostic_.reports.size : 0
}

const activeMark = Decoration.mark({ class: 'cm-diagnosticRange cm-diagnosticRange-active' })

function diagnosticTooltip(view: EditorView, pos: number, side: -1 | 1) {
Expand Down Expand Up @@ -249,24 +214,6 @@ function reportsTooltip(view: EditorView, reports: readonly Report[]) {
)
}

/// Command to open and focus the diagnostic panel.
export const openDiagnosticPanel: Command = (view: EditorView) => {
let field = view.state.field(diagnosticState, false)
if (!field || !field.panel)
view.dispatch({ effects: maybeEnableDiagnostic(view.state, [togglePanel.of(true)]) })
let panel = getPanel(view, DiagnosticPanel.open)
if (panel) (panel.dom.querySelector('.cm-panel-diagnostic ul') as HTMLElement).focus()
return true
}

/// Command to close the diagnostic panel, when open.
export const closeDiagnosticPanel: Command = (view: EditorView) => {
let field = view.state.field(diagnosticState, false)
if (!field || !field.panel) return false
view.dispatch({ effects: togglePanel.of(false) })
return true
}

/// Move the selection to the next report.
export const nextReport: Command = (view: EditorView) => {
let field = view.state.field(diagnosticState, false)
Expand Down Expand Up @@ -311,12 +258,8 @@ export const previousReport: Command = (view: EditorView) => {

/// A set of default key bindings for the diagnostic functionality.
///
/// - Ctrl-Shift-m (Cmd-Shift-m on macOS): [`openDiagnosticPanel`](#diagnostic.openDiagnosticPanel)
/// - F8: [`nextReport`](#diagnostic.nextReport)
export const diagnosticKeymap: readonly KeyBinding[] = [
{ key: 'Mod-Shift-m', run: openDiagnosticPanel, preventDefault: true },
{ key: 'F8', run: nextReport },
]
export const diagnosticKeymap: readonly KeyBinding[] = [{ key: 'F8', run: nextReport }]

const diagnosticConfig = Facet.define<{ config: DiagnosticConfig }, Required<DiagnosticConfig>>({
combine(input) {
Expand All @@ -336,64 +279,12 @@ export function createDiagnostics(config: DiagnosticConfig = {}): Extension {
return [diagnosticConfig.of({ config }), diagnosticExtensions]
}

function assignKeys(actions: readonly Action[] | undefined) {
let assigned: string[] = []
if (actions)
actions: for (let { name } of actions) {
for (let i = 0; i < name.length; i++) {
let ch = name[i]
if (/[a-zA-Z]/.test(ch) && !assigned.some((c) => c.toLowerCase() == ch.toLowerCase())) {
assigned.push(ch)
continue actions
}
}
assigned.push('')
}
return assigned
}

function renderReport(view: EditorView, report: Report, inPanel: boolean) {
let keys = inPanel ? assignKeys(report.actions) : []
function renderReport(view: EditorView, report: Report, _inPanel: boolean) {
return elt(
'li',
{ class: 'cm-report cm-report-' + report.severity },
elt(
'span',
{ class: 'cm-reportText' },
report.renderMessage ? report.renderMessage() : report.message,
),
report.actions?.map((action, i) => {
let fired = false,
click = (e: Event) => {
e.preventDefault()
if (fired) return
fired = true
let found = findReport(view.state.field(diagnosticState).reports, report)
if (found) action.apply(view, found.from, found.to)
}
let { name } = action,
keyIndex = keys[i] ? name.indexOf(keys[i]) : -1
let nameElt =
keyIndex < 0
? name
: [
name.slice(0, keyIndex),
elt('u', name.slice(keyIndex, keyIndex + 1)),
name.slice(keyIndex + 1),
]
return elt(
'button',
{
type: 'button',
class: 'cm-reportAction',
onclick: click,
onmousedown: click,
'aria-label': ` Action: ${name}${keyIndex < 0 ? '' : ` (access key "${keys[i]})"`}.`,
},
nameElt,
)
}),
report.sourceId && elt('div', { class: 'cm-reportSource' }, report.sourceId),
elt('span', { class: 'cm-reportText' }, report.message),
report.span.sourceId && elt('div', { class: 'cm-reportSource' }, report.span.sourceId),
)
}

Expand All @@ -411,11 +302,15 @@ class ReportWidget extends WidgetType {
}
}

/*
class PanelItem {
id = 'item_' + Math.floor(Math.random() * 0xffffffff).toString(16)
dom: HTMLElement

constructor(view: EditorView, readonly report: Report) {
constructor(
view: EditorView,
readonly report: Report,
) {
this.dom = renderReport(view, report, true)
this.dom.id = this.id
this.dom.setAttribute('role', 'option')
Expand Down Expand Up @@ -448,15 +343,6 @@ class DiagnosticPanel implements Panel {
} else if (event.keyCode == 13) {
// Enter
this.view.focus()
} else if (event.keyCode >= 65 && event.keyCode <= 90 && this.selectedIndex >= 0) {
// A-Z
let { report } = this.items[this.selectedIndex],
keys = assignKeys(report.actions)
for (let i = 0; i < keys.length; i++)
if (keys[i].toUpperCase().charCodeAt(0) == event.keyCode) {
let found = findReport(this.view.state.field(diagnosticState).reports, report)
if (found) report.actions![i].apply(view, found.from, found.to)
}
} else {
return
}
Expand Down Expand Up @@ -521,7 +407,10 @@ class DiagnosticPanel implements Panel {
}
i++
})
while (i < this.items.length && !(this.items.length == 1 && this.items[0].report.from < 0)) {
while (
i < this.items.length &&
!(this.items.length == 1 && this.items[0].report.span.from < 0)
) {
needsSync = true
this.items.pop()
}
Expand Down Expand Up @@ -581,6 +470,7 @@ class DiagnosticPanel implements Panel {
return new DiagnosticPanel(view)
}
}
*/

function severityWeight(sev: Severity) {
return sev == 'error' ? 4 : sev == 'warning' ? 3 : sev == 'info' ? 2 : 1
Expand Down Expand Up @@ -677,7 +567,7 @@ function gutterMarkerMouseOver(view: EditorView, marker: HTMLElement, reports: r
function markersForReports(doc: Text, reports: readonly Report[]) {
let byLine: { [line: number]: Report[] } = Object.create(null)
for (let report of reports) {
let line = doc.lineAt(report.from)
let line = doc.lineAt(report.span.from)
;(byLine[line.from] || (byLine[line.from] = [])).push(report)
}
let markers: Range<GutterMarker>[] = []
Expand Down Expand Up @@ -729,8 +619,8 @@ const diagnosticGutterTooltip = StateField.define<Tooltip | null>({
const diagnosticExtensions = [
diagnosticState,
EditorView.decorations.compute([diagnosticState], (state) => {
let { selected, panel } = state.field(diagnosticState)
return !selected || !panel || selected.from == selected.to
let { selected } = state.field(diagnosticState)
return !selected || selected.from == selected.to
? Decoration.none
: Decoration.set([activeMark.range(selected.from, selected.to)])
}),
Expand Down
Loading
Loading