317 lines
8.2 KiB
TypeScript
317 lines
8.2 KiB
TypeScript
import { AbstractEditor, EditorDelegate, Marker } from "./types";
|
|
import { basicSetup } from "codemirror";
|
|
import {
|
|
indentMore,
|
|
indentLess,
|
|
toggleLineComment,
|
|
} from "@codemirror/commands";
|
|
import { EditorView, hoverTooltip, keymap, Tooltip } from "@codemirror/view";
|
|
import { Compartment, Prec } from "@codemirror/state";
|
|
import { oneDark } from "@codemirror/theme-one-dark";
|
|
import { linter } from "@codemirror/lint";
|
|
import {
|
|
bracketMatching,
|
|
indentService,
|
|
LanguageSupport,
|
|
StreamLanguage,
|
|
StringStream,
|
|
} from "@codemirror/language";
|
|
import { ABBREV } from "./abbrev.js";
|
|
|
|
// prettier flattened this...
|
|
const keywords = [
|
|
"let",
|
|
"in",
|
|
"where",
|
|
"case",
|
|
"of",
|
|
"data",
|
|
"derive",
|
|
"U",
|
|
"do",
|
|
"ptype",
|
|
"pfunc",
|
|
"module",
|
|
"infixl",
|
|
"infixr",
|
|
"infix",
|
|
"∀",
|
|
"forall",
|
|
"import",
|
|
"uses",
|
|
"class",
|
|
"instance",
|
|
"record",
|
|
"constructor",
|
|
"if",
|
|
"then",
|
|
"else",
|
|
"$",
|
|
"λ",
|
|
"?",
|
|
"@",
|
|
".",
|
|
"->",
|
|
"→",
|
|
":",
|
|
"=>",
|
|
":=",
|
|
"$=",
|
|
"=",
|
|
"<-",
|
|
"\\",
|
|
"_",
|
|
"|",
|
|
];
|
|
|
|
// a stack of tokenizers, current is first
|
|
// we need to push / pop {} so we can parse strings correctly
|
|
interface State {
|
|
tokenizers: Tokenizer[];
|
|
}
|
|
type Tokenizer = (stream: StringStream, state: State) => string | null;
|
|
|
|
function tokenizer(stream: StringStream, state: State): string | null {
|
|
if (stream.eatSpace()) return null;
|
|
if (stream.match("--")) {
|
|
stream.skipToEnd();
|
|
return "comment";
|
|
}
|
|
// maybe keyword?
|
|
if (stream.match(/{/)) {
|
|
state.tokenizers.unshift(tokenizer);
|
|
return null;
|
|
}
|
|
if (stream.match(/}/) && state.tokenizers.length > 1) {
|
|
state.tokenizers.shift();
|
|
return state.tokenizers[0] === stringTokenizer ? "keyword" : null;
|
|
}
|
|
if (stream.match(/^[/]-/)) {
|
|
state.tokenizers.unshift(commentTokenizer);
|
|
return state.tokenizers[0](stream, state);
|
|
}
|
|
if (stream.match(/"/)) {
|
|
state.tokenizers.unshift(stringTokenizer);
|
|
return stringTokenizer(stream, state);
|
|
}
|
|
// TODO match tokenizer better..
|
|
if (stream.match(/[^\\(){}[\],.@;\s][^()\\{}\[\],.@;\s]*/)) {
|
|
let word = stream.current();
|
|
if (keywords.includes(word)) return "keyword";
|
|
if (word[0] >= "A" && word[0] <= "Z") return "typeName";
|
|
return "variableName";
|
|
}
|
|
// unhandled
|
|
stream.next();
|
|
return null;
|
|
}
|
|
|
|
function stringTokenizer(stream: StringStream, state: State) {
|
|
while (true) {
|
|
if (stream.current() && stream.match(/^\\{/, false)) {
|
|
return "string";
|
|
}
|
|
if (stream.match(/^\\{/)) {
|
|
state.tokenizers.unshift(tokenizer);
|
|
return "keyword";
|
|
}
|
|
let ch = stream.next();
|
|
if (!ch) return "string";
|
|
if (ch === '"') {
|
|
state.tokenizers.shift();
|
|
return "string";
|
|
}
|
|
}
|
|
}
|
|
|
|
// We have a tokenizer for this because codemirror processes a line at a time.
|
|
// So we may need to end the line in `comment` state and see the -/ later
|
|
function commentTokenizer(stream: StringStream, state: State): string | null {
|
|
console.log("ctok");
|
|
let dash = false;
|
|
let ch;
|
|
while ((ch = stream.next())) {
|
|
if (dash && ch === "/") {
|
|
state.tokenizers.shift();
|
|
return "comment";
|
|
}
|
|
dash = ch === "-";
|
|
}
|
|
return "comment";
|
|
}
|
|
|
|
const newtLanguage2: StreamLanguage<State> = StreamLanguage.define({
|
|
startState: () => ({ tokenizers: [tokenizer] }),
|
|
token(stream, st) {
|
|
return st.tokenizers[0](stream, st);
|
|
},
|
|
languageData: {
|
|
commentTokens: {
|
|
line: "--",
|
|
},
|
|
// The real list would include almost every character.
|
|
wordChars: "!#$%^&*_+-=<>|",
|
|
},
|
|
});
|
|
|
|
export function scheme() {
|
|
return new LanguageSupport(schemeLanguage);
|
|
}
|
|
|
|
const schemeLanguage: StreamLanguage<State> = StreamLanguage.define({
|
|
startState: () => null,
|
|
token(stream, st) {
|
|
const keywords = ["define", "let", "case", "cond", "import", "include", "lambda", "else"];
|
|
if (stream.eatSpace()) return null;
|
|
if (stream.match("--")) {
|
|
stream.skipToEnd();
|
|
return "comment";
|
|
}
|
|
if (stream.match(/[0-9A-Za-z!%&*+./:<=>?@^_~-]+/)) {
|
|
let word = stream.current();
|
|
if (keywords.includes(word)) return "keyword";
|
|
return null;
|
|
}
|
|
// unhandled
|
|
stream.next();
|
|
return null
|
|
},
|
|
languageData: {
|
|
commentTokens: {
|
|
line: ";;",
|
|
},
|
|
wordChars: "!%&*+-./:<=>?@^_~",
|
|
},
|
|
});
|
|
|
|
function newt() {
|
|
return new LanguageSupport(newtLanguage2);
|
|
}
|
|
|
|
export class CMEditor implements AbstractEditor {
|
|
view: EditorView;
|
|
delegate: EditorDelegate;
|
|
theme: Compartment;
|
|
constructor(container: HTMLElement, doc: string, delegate: EditorDelegate) {
|
|
this.delegate = delegate;
|
|
this.theme = new Compartment();
|
|
|
|
this.view = new EditorView({
|
|
doc,
|
|
parent: container,
|
|
extensions: [
|
|
basicSetup,
|
|
linter((view) => this.delegate.lint(view)),
|
|
// For indent on return
|
|
indentService.of((ctx, pos) => {
|
|
let line = ctx.lineAt(pos);
|
|
if (!line || !line.from) return null;
|
|
let prevLine = ctx.lineAt(line.from - 1);
|
|
if (prevLine.text.trimEnd().match(/\b(of|where|do)\s*$/)) {
|
|
let pindent = prevLine.text.match(/^\s*/)?.[0].length ?? 0;
|
|
return pindent + 2;
|
|
}
|
|
return null;
|
|
}),
|
|
Prec.highest(
|
|
keymap.of([
|
|
{
|
|
key: "Tab",
|
|
preventDefault: true,
|
|
run: indentMore,
|
|
},
|
|
{
|
|
key: "Cmd-/",
|
|
run: toggleLineComment,
|
|
},
|
|
// ok, we can do multiple keys (we'll want this for Idris)
|
|
{
|
|
key: "c-c c-s",
|
|
run: () => {
|
|
console.log("C-c C-s");
|
|
return false;
|
|
},
|
|
},
|
|
{
|
|
key: "Shift-Tab",
|
|
preventDefault: true,
|
|
run: indentLess,
|
|
},
|
|
])
|
|
),
|
|
EditorView.updateListener.of((update) => {
|
|
let doc = update.state.doc;
|
|
update.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
|
|
if (" ')\\_".includes(inserted.toString()) || inserted.lines > 1) {
|
|
console.log("changes", update.changes, update.changes.desc);
|
|
let line = doc.lineAt(fromA);
|
|
let e = fromA - line.from;
|
|
const m = line.text.slice(0, e).match(/(\\[^ ]+)$/);
|
|
if (m) {
|
|
let s = e - m[0].length;
|
|
let key = line.text.slice(s, e);
|
|
if (ABBREV[key]) {
|
|
this.view.dispatch({
|
|
changes: {
|
|
from: line.from + s,
|
|
to: fromA,
|
|
insert: ABBREV[key],
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}),
|
|
this.theme.of(EditorView.baseTheme({})),
|
|
hoverTooltip(async (view, pos) => {
|
|
let cursor = this.view.state.doc.lineAt(pos);
|
|
let line = cursor.number - 1;
|
|
let col = pos - cursor.from;
|
|
// let range = this.view.state.wordAt(pos);
|
|
console.log('getting hover for ',line, col);
|
|
let entry = await this.delegate.getEntry('', line, col)
|
|
|
|
if (entry) {
|
|
let rval: Tooltip = {
|
|
// TODO pull in position from LSP (currently it only has the jump-to FC)
|
|
pos,
|
|
above: true,
|
|
create: () => {
|
|
let dom = document.createElement("div");
|
|
dom.className = "tooltip";
|
|
dom.textContent = entry.info;
|
|
return { dom };
|
|
},
|
|
};
|
|
return rval;
|
|
}
|
|
|
|
// we'll iterate the syntax tree for word.
|
|
// let entry = delegate.getEntry(word, line, col)
|
|
return null;
|
|
}),
|
|
newt(),
|
|
],
|
|
});
|
|
}
|
|
setDark(isDark: boolean) {
|
|
this.view.dispatch({
|
|
effects: this.theme.reconfigure(
|
|
isDark ? oneDark : EditorView.baseTheme({})
|
|
),
|
|
});
|
|
}
|
|
setValue(_doc: string) {
|
|
// Is this all we can do?
|
|
this.view.dispatch({
|
|
changes: { from: 0, to: this.view.state.doc.length, insert: _doc },
|
|
});
|
|
}
|
|
getValue() {
|
|
// maybe?
|
|
return this.view.state.doc.toString();
|
|
}
|
|
setMarkers(_: Marker[]) {}
|
|
}
|