Skip to content

Commit

Permalink
use esphome language (#562)
Browse files Browse the repository at this point in the history
* use esphome language

this migrates from yaml language to esphome language in the monaco editor, this mostly deals with changing how tokens are named, more specifically keys in yaml blocks are no longer text but keywords, this results in the editor triggering a completion dropdown to show up as soon as a character is written. This has a major impact on editing on mobile as there is no easy way to trigger the auto completions

* fix format
  • Loading branch information
glmnet authored Feb 24, 2024
1 parent ce2a83c commit eab2f6d
Show file tree
Hide file tree
Showing 2 changed files with 258 additions and 4 deletions.
3 changes: 2 additions & 1 deletion src/editor/esphome-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ class ESPHomeEditor extends LitElement {
});
this.editor = monaco.editor.create(this.container, {
value: "",
language: "yaml",
language: "esphome",
theme: darkQuery.matches ? "esphome-dark" : "esphome",
minimap: {
enabled: false,
Expand Down Expand Up @@ -196,6 +196,7 @@ class ESPHomeEditor extends LitElement {
if (!this.editorValidationScheduled || this.editorValidationRunning)
return;
if (this.editorActiveWebSocket == null) return;
if (this.editorActiveWebSocket.readyState != 1) return;

this.sendAceStdin({
type: "validate",
Expand Down
259 changes: 256 additions & 3 deletions src/editor/monaco-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import { TextBuffer } from "./utils/text-buffer";

const documents = new ESPHomeDocuments();

// Register the custom language
monaco.languages.register({ id: "esphome" });

const hoverHandler = new HoverHandler(documents);
monaco.languages.registerHoverProvider("yaml", {
monaco.languages.registerHoverProvider("esphome", {
provideHover: async function (model, position) {
documents.update(model.uri.toString(), new TextBuffer(model));
const hover = await hoverHandler.getHover(
Expand All @@ -21,7 +24,7 @@ monaco.languages.registerHoverProvider("yaml", {
});

const completionsHandler = new CompletionsHandler(documents);
monaco.languages.registerCompletionItemProvider("yaml", {
monaco.languages.registerCompletionItemProvider("esphome", {
provideCompletionItems: async function (model, position) {
documents.update(model.uri.toString(), new TextBuffer(model));
const completions = await completionsHandler.getCompletions(
Expand All @@ -33,7 +36,7 @@ monaco.languages.registerCompletionItemProvider("yaml", {
});

const definitionHandler = new DefinitionHandler(documents);
monaco.languages.registerDefinitionProvider("yaml", {
monaco.languages.registerDefinitionProvider("esphome", {
provideDefinition: async function (model, position) {
documents.update(model.uri.toString(), new TextBuffer(model));
const ret = await definitionHandler.getDefinition(
Expand Down Expand Up @@ -73,3 +76,253 @@ monaco.editor.defineTheme("esphome-dark", {
"editor.foreground": "#E8E8E9",
},
});

monaco.languages.setLanguageConfiguration("esphome", {
comments: {
lineComment: "#",
},
brackets: [
["{", "}"],
["[", "]"],
["(", ")"],
],
autoClosingPairs: [
{ open: "{", close: "}" },
{ open: "[", close: "]" },
{ open: "(", close: ")" },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
surroundingPairs: [
{ open: "{", close: "}" },
{ open: "[", close: "]" },
{ open: "(", close: ")" },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
folding: {
offSide: true,
},
onEnterRules: [
{
beforeText: /:\s*$/,
action: {
indentAction: monaco.languages.IndentAction.Indent,
},
},
],
});

monaco.languages.setMonarchTokensProvider("esphome", {
tokenPostfix: ".esphome",

brackets: [
{ token: "delimiter.bracket", open: "{", close: "}" },
{ token: "delimiter.square", open: "[", close: "]" },
],

keywords: [
"true",
"True",
"TRUE",
"false",
"False",
"FALSE",
"null",
"Null",
"Null",
"~",
],

numberInteger: /(?:0|[+-]?[0-9]+)/,
numberFloat: /(?:0|[+-]?[0-9]+)(?:\.[0-9]+)?(?:e[-+][1-9][0-9]*)?/,
numberOctal: /0o[0-7]+/,
numberHex: /0x[0-9a-fA-F]+/,
numberInfinity: /[+-]?\.(?:inf|Inf|INF)/,
numberNaN: /\.(?:nan|Nan|NAN)/,
numberDate:
/\d{4}-\d\d-\d\d([Tt ]\d\d:\d\d:\d\d(\.\d+)?(( ?[+-]\d\d?(:\d\d)?)|Z)?)?/,

escapes: /\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,

tokenizer: {
root: [
{ include: "@whitespace" },
{ include: "@comment" },

// Directive
[/%[^ ]+.*$/, "meta.directive"],

// Document Markers
[/---/, "operators.directivesEnd"],
[/\.{3}/, "operators.documentEnd"],

// Block Structure Indicators
[/[-?:](?= )/, "operators"],

{ include: "@anchor" },
{ include: "@tagHandle" },
{ include: "@flowCollections" },
{ include: "@blockStyle" },

// Numbers
[/@numberInteger(?![ \t]*\S+)/, "number"],
[/@numberFloat(?![ \t]*\S+)/, "number.float"],
[/@numberOctal(?![ \t]*\S+)/, "number.octal"],
[/@numberHex(?![ \t]*\S+)/, "number.hex"],
[/@numberInfinity(?![ \t]*\S+)/, "number.infinity"],
[/@numberNaN(?![ \t]*\S+)/, "number.nan"],
[/@numberDate(?![ \t]*\S+)/, "number.date"],

// Key:Value pair
[
/(".*?"|'.*?'|[^#'"]*?)([ \t]*)(:)( |$)/,
["type", "white", "operators", "white"],
],

{ include: "@flowScalars" },

// String nodes
[
/.+?(?=(\s+#|$))/,
{
cases: {
"@keywords": "keyword",
"@default": "type",
},
},
],
],

// Flow Collection: Flow Mapping
object: [
{ include: "@whitespace" },
{ include: "@comment" },

// Flow Mapping termination
[/\}/, "@brackets", "@pop"],

// Flow Mapping delimiter
[/,/, "delimiter.comma"],

// Flow Mapping Key:Value delimiter
[/:(?= )/, "operators"],

// Flow Mapping Key:Value key
[/(?:".*?"|'.*?'|[^,\{\[]+?)(?=: )/, "type"],

// Start Flow Style
{ include: "@flowCollections" },
{ include: "@flowScalars" },

// Scalar Data types
{ include: "@tagHandle" },
{ include: "@anchor" },
{ include: "@flowNumber" },

// Other value (keyword or string)
[
/[^\},]+/,
{
cases: {
"@keywords": "keyword",
"@default": "string",
},
},
],
],

// Flow Collection: Flow Sequence
array: [
{ include: "@whitespace" },
{ include: "@comment" },

// Flow Sequence termination
[/\]/, "@brackets", "@pop"],

// Flow Sequence delimiter
[/,/, "delimiter.comma"],

// Start Flow Style
{ include: "@flowCollections" },
{ include: "@flowScalars" },

// Scalar Data types
{ include: "@tagHandle" },
{ include: "@anchor" },
{ include: "@flowNumber" },

// Other value (keyword or string)
[
/[^\],]+/,
{
cases: {
"@keywords": "keyword",
"@default": "identifier",
},
},
],
],

// First line of a Block Style
multiString: [[/^( +).+$/, "string", "@multiStringContinued.$1"]],

// Further lines of a Block Style
// Workaround for indentation detection
multiStringContinued: [
[
/^( *).+$/,
{
cases: {
"$1==$S2": "string",
"@default": { token: "@rematch", next: "@popall" },
},
},
],
],

whitespace: [[/[ \t\r\n]+/, "white"]],

// Only line comments
comment: [[/#.*$/, "comment"]],

// Start Flow Collections
flowCollections: [
[/\[/, "@brackets", "@array"],
[/\{/, "@brackets", "@object"],
],

// Start Flow Scalars (quoted strings)
flowScalars: [
[/"([^"\\]|\\.)*$/, "string.invalid"],
[/'([^'\\]|\\.)*$/, "string.invalid"],
[/'[^']*'/, "string"],
[/"/, "string", "@doubleQuotedString"],
],

doubleQuotedString: [
[/[^\\"]+/, "string"],
[/@escapes/, "string.escape"],
[/\\./, "string.escape.invalid"],
[/"/, "string", "@pop"],
],

// Start Block Scalar
blockStyle: [[/[>|][0-9]*[+-]?$/, "operators", "@multiString"]],

// Numbers in Flow Collections (terminate with ,]})
flowNumber: [
[/@numberInteger(?=[ \t]*[,\]\}])/, "number"],
[/@numberFloat(?=[ \t]*[,\]\}])/, "number.float"],
[/@numberOctal(?=[ \t]*[,\]\}])/, "number.octal"],
[/@numberHex(?=[ \t]*[,\]\}])/, "number.hex"],
[/@numberInfinity(?=[ \t]*[,\]\}])/, "number.infinity"],
[/@numberNaN(?=[ \t]*[,\]\}])/, "number.nan"],
[/@numberDate(?=[ \t]*[,\]\}])/, "number.date"],
],

tagHandle: [[/\![^ ]*/, "tag"]],

anchor: [[/[&*][^ ]+/, "namespace"]],
},
});

0 comments on commit eab2f6d

Please sign in to comment.