{"id":"2c3cf8464aed9c1f","slug":"hello-lean-prover","trashed":false,"description":"","likes":38,"publish_level":"public","forks":2,"fork_of":null,"has_importers":true,"update_time":"2021-01-26T17:37:23.351Z","first_public_version":null,"paused_version":null,"publish_time":"2019-07-28T19:34:54.661Z","publish_version":5348,"latest_version":5348,"thumbnail":"90ec3d3750f7d06c21ecc29f59b558cfe3ad9f18501b1ede75c7d05d0bd66774","default_thumbnail":"8dc01e380330c9de53d8fd3ef75224fa4d3d39448c897c806b48c01efc5b4bd8","roles":[],"sharing":null,"owner":{"id":"d2fd23101beb2a61","avatar_url":"https://avatars.observableusercontent.com/avatar/73f0dccc0b7fc5c845e6a52a58e01a2ae432cc529aaada0d10ab602137e603c0","login":"bryangingechen","name":"Bryan Gin-ge Chen","bio":"here to learn","home_url":"https://twitter.com/blockspins","type":"team","tier":"starter_2024"},"creator":{"id":"5c13a1a27cf52564","avatar_url":"https://avatars.observableusercontent.com/avatar/73f0dccc0b7fc5c845e6a52a58e01a2ae432cc529aaada0d10ab602137e603c0","login":"bryangingechen","name":"Bryan Gin-ge Chen","bio":"here to learn","home_url":"https://twitter.com/blockspins","tier":"public"},"authors":[{"id":"5c13a1a27cf52564","avatar_url":"https://avatars.observableusercontent.com/avatar/73f0dccc0b7fc5c845e6a52a58e01a2ae432cc529aaada0d10ab602137e603c0","name":"Bryan Gin-ge Chen","login":"bryangingechen","bio":"here to learn","home_url":"https://twitter.com/blockspins","tier":"public","approved":true,"description":""}],"collections":[{"id":"68b035771823d0b5","type":"public","slug":"lean","title":"Lean","description":"Notebooks related to the Lean theorem prover https://leanprover.github.io","update_time":"2019-12-14T08:32:41.501Z","pinned":false,"ordered":true,"custom_thumbnail":"e31ecdc4b6bd07378620dcddfaefecb887a104cb84645085ff32760cf4f3d2a4","default_thumbnail":"90ec3d3750f7d06c21ecc29f59b558cfe3ad9f18501b1ede75c7d05d0bd66774","thumbnail":"e31ecdc4b6bd07378620dcddfaefecb887a104cb84645085ff32760cf4f3d2a4","listing_count":10,"parent_collection_count":0,"owner":{"id":"d2fd23101beb2a61","avatar_url":"https://avatars.observableusercontent.com/avatar/73f0dccc0b7fc5c845e6a52a58e01a2ae432cc529aaada0d10ab602137e603c0","login":"bryangingechen","name":"Bryan Gin-ge Chen","bio":"here to learn","home_url":"https://twitter.com/blockspins","type":"team","tier":"starter_2024"}},{"id":"80583d7c09f19038","type":"public","slug":"inputs","title":"Ingredients","description":"duty-free imports of inputs, tools, and libraries!","update_time":"2019-03-04T21:23:36.451Z","pinned":false,"ordered":false,"custom_thumbnail":"deca5a470fcbf5a75aa2e7c987fba0f36983be403c13315876f6cc150be0c7cb","default_thumbnail":"fcde2368f35e13a9ab2dd16547487c94be80963caefde9b3a926d575b729d411","thumbnail":"deca5a470fcbf5a75aa2e7c987fba0f36983be403c13315876f6cc150be0c7cb","listing_count":24,"parent_collection_count":0,"owner":{"id":"d2fd23101beb2a61","avatar_url":"https://avatars.observableusercontent.com/avatar/73f0dccc0b7fc5c845e6a52a58e01a2ae432cc529aaada0d10ab602137e603c0","login":"bryangingechen","name":"Bryan Gin-ge Chen","bio":"here to learn","home_url":"https://twitter.com/blockspins","type":"team","tier":"starter_2024"}}],"files":[],"comments":[],"commenting_lock":null,"suggestion_from":null,"suggestions_to":[],"version":5348,"title":"Hello, Lean prover!","license":null,"copyright":"","nodes":[{"id":0,"value":"md`# Hello, Lean prover!\n\n<figure style='max-width:500px'>\n<img src=\"https://imgur.com/3raLIB2.png\" />\n</figure>`","pinned":false,"mode":"js","data":null,"name":null},{"id":1227,"value":"md`The [Lean theorem prover](https://leanprover.github.io/) is an open source [interactive theorem prover](https://en.wikipedia.org/wiki/Proof_assistant) developed at Microsoft Research. The Lean project was launched in 2013 by [Leonardo de Moura](http://leodemoura.github.io/) (of [Z3](https://github.com/Z3Prover/z3) fame) and so it is a relatively new entrant to the field ([Coq](https://coq.inria.fr/), [Isabelle](https://isabelle.in.tum.de/), [Agda](https://wiki.portal.chalmers.se/agda/pmwiki.php) are a few others). \n\nA fun way to start playing around in Lean is Kevin Buzzard's [Natural Number Game](http://wwwf.imperial.ac.uk/~buzzard/xena/natural_number_game/).\n\nThese three online books are also good sources for introductory material about Lean:\n- [\"Theorem Proving in Lean\"](https://leanprover.github.io/theorem_proving_in_lean/) by Jeremy Avigad, Leonardo de Moura and Soonho Kong\n- [\"Logic and Proof\"](https://avigad.github.io/logic_and_proof/) by Jeremy Avigad, Robert Y. Lewis, and Floris van Doorn.\n- Lecture notes to [the course \"Logical Verification in Lean\"](https://lean-forward.github.io/logical-verification/2019/) by Tim Baanen, Alexander Bentkamp, Jasmin Blanchette and Johannes Hölzl.\n\nThe Lean developers have put in quite a bit of effort to make Lean usable in the browser. For instance, the Natural Number Game runs in the browser and the first two books above have snippets of Lean code which link to the [\\`lean-web-editor\\`](https://leanprover.github.io/live/latest), a web app that runs an Emscripten build of Lean. In light of my positive experiences writing interactive documents in Observable, I thought that it might be fun to explore whether Observable notebooks + Lean could become a viable option for explaining and sharing formalized math on the web.\n\n⚠️ The WASM version of Lean does not work in Safari 13.0 (see [this WebKit bug](https://bugs.webkit.org/show_bug.cgi?id=201028)). Please upgrade to Safari 13.1 or try Firefox or Chrome instead!\n\nHere's an instance of the [Lean CodeMirror editor](#leanEditor) from this notebook. Try hovering and moving the cursor around, and then try editing (the full features are described [below](#CMLeanEditor)):\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":809,"value":"viewof test0 = leanEditor({\n  fileName: 'test0.lean',\n  value: String.raw`variables p q r s : Prop\ntheorem my_and_comm : p /\\ q <-> q /\\ p :=\niff.intro\n(assume Hpq : p /\\ q,\nand.intro (and.elim_right Hpq) (and.elim_left Hpq))\n(assume Hqp : q /\\ p,\nand.intro (and.elim_right Hqp) (and.elim_left Hqp))\n#check @nat.rec_on\n#print \"end of file!\"\n`,\n  height: '200px',\n}, invalidation)","pinned":false,"mode":"js","data":null,"name":null},{"id":1400,"value":"md`The border changes color depending on the server status:\n- <span style=\"padding: 1px; border-style: solid; border-color: ${config.initColor}\">the editor is initializing</span>\n- <span style=\"padding: 1px; border-style: solid; border-color: ${config.runningColor}\">Lean is busy</span>\n- <span style=\"padding: 1px; border-style: solid; border-color: ${config.doneColor}\">Lean is ready</span>\n- <span style=\"padding: 1px; border-style: solid; border-color: ${config.stoppedColor}\">the Lean server has stopped and needs to be reset</span>.\n\nThe Lean editor cells are [views](https://observablehq.com/@observablehq/introduction-to-views), and their values are objects which contain (among other things) the responses from the Lean server, allowing data to be passed from Lean to Observable:\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":1535,"value":"test0.msgs","pinned":true,"mode":"js","data":null,"name":null},{"id":1532,"value":"md`This notebook also provides [a tag function](#leanHL) for syntax-highlighting (but not actually processing) Lean code like this:\n\n${leanHL`def collatz_fn (n : ℕ) : ℕ :=\nif (2 ∣ n) then n / 2 else 3 * n + 1\n\n#eval collatz_fn 0\n\ntheorem collatz_conjecture (n : ℕ) : n > 0 → ∃ m,\n  nat.iterate collatz_fn m n = 4 := sorry\n\n#eval nat.iterate collatz_fn 10 7`}`","pinned":false,"mode":"js","data":null,"name":null},{"id":1391,"value":"md`This notebook primarily consists of the code gluing everything together along with some notes documenting how it works (it assumes [basic knowledge about Observable](https://observablehq.com/@observablehq/observable-user-manual)). It's meant more for reference / hacking than for education or entertainment.\n\n<div style=\"border-radius: 5px; border: 1px solid; max-width:640px; padding: 1em;\">👉 Check out the companion notebooks [\"Fibonacci formalized\"](https://observablehq.com/@bryangingechen/fibonacci-formalized-1-some-sums) to see some formalized mathematics written using the functions here.</div>\n\nFor convenience, here's an import command that you can copy and paste (you can delete any items in the list that you don't use directly):\n\n\\`\\`\\`js\nimport {leanEditor, leanEditorFrom, InfoView, LeanFile, leanControl, leanHL, parseLeanMsgs}\n  from '@bryangingechen/hello-lean-prover'\n\\`\\`\\`\n\n⚡️ If you already have Lean installed on your computer, you can try using that as a backend for the Lean editor (instead of relying on the much slower Emscripten build) by following the directions [here](https://observablehq.com/@bryangingechen/lean-websocketd)!\n\nFeel free to make suggestions on the notebook using Observable's \"suggest\" feature or by messaging me [on twitter](https://twitter.com/blockspins). For questions about Lean, check out [the Lean zulip chat](https://leanprover.zulipchat.com/)! Questions at all levels are welcomed; I certainly learned tons from the folks there!\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":3829,"value":"md`### Related work:\n\nThe functions and cells below integrate the Emscripten-compiled JS and WebAssembly versions of Lean with the [CodeMirror editor](https://codemirror.net/index.html). This notebook builds heavily on (lifts code from) the [\\`lean-client-js\\`](https://github.com/leanprover/lean-client-js), [\\`lean-web-editor\\`](https://github.com/leanprover/lean-web-editor) and [\\`vscode-lean\\`](https://github.com/leanprover/vscode-lean) projects by Gabriel Ebner, Jared Roesch, Sebastian Ulrich and other contributors.\n\nPatrick Massot's [\\`format-lean\\`](https://github.com/leanprover-community/format_lean) project also aims to make Lean code presentable on the web (see [this beautiful example](https://www.math.u-psud.fr/~pmassot/lean/) and [these course notes (in French)](https://www.math.u-psud.fr/~pmassot/enseignement/math114/)). The approach there is to generate HTML & JS documents from commented Lean files.\n\nThe [CoCalc](https://cocalc.com) project also has code that integrates [Lean with Codemirror](https://github.com/sagemathinc/cocalc/tree/67c3f43a812e88d2a3f761c90e63897a81fe96a7/src/smc-webapp/frame-editors/lean-editor), including a [\"Lean mode\"](https://github.com/sagemathinc/cocalc/blob/d0124719b36621bc03d8becb256b300f255c05ac/src/smc-webapp/codemirror/mode/lean.ts). The focus of that project is on collaboratively editing and saving files hosted on their servers. The CoCalc Lean editor communicates with [Lean running on their servers](https://github.com/sagemathinc/cocalc/tree/67c3f43a812e88d2a3f761c90e63897a81fe96a7/src/smc-project/lean) rather than with Emscripten Lean running in the browser.\n\nWylie Conlon's [\\`lsp-editor-adapter\\`](https://github.com/wylieconlon/lsp-editor-adapter) is an interesting project which aims to let CodeMirror editors communicate with servers that speak the \"language server protocol\". Sadly, the Lean server doesn't use the LSP directly (though there is an implementation of a [Lean language server](https://github.com/leanprover/lean-client-js/tree/master/lean-language-server)).`","pinned":false,"mode":"js","data":null,"name":null},{"id":1864,"value":"Contents = md`## Contents\n\n1. [Starting the Lean server](#Starting)\n2. [Controlling the Lean server](#Controlling)\n  - [Server control form](#ServerForm)\n  - [Server commands](#ServerCommands)\n  - [Range of interest syncing](#RangeOfInterest)\n  - [Lean file objects](#LeanFileDoc)\n3. [CodeMirror editor](#CodeMirrorEditor)\n  - [Lean mode](#CMLeanMode)\n  - [Lean editor](#CMLeanEditor)\n    - [Usage](#CMLeanEditorUsage)\n    - [Examples](#CMLeanEditorExamples)\n  - [Global config object](#ConfigDoc)\n  - [Auxiliary functions](#AuxFunctions)\n  - [CSS](#CodeMirrorCSS)\n4. [TODO / Version history](#TODO)\n5. [LICENSE](#LICENSE)\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":1235,"value":"Starting = md`## 1. Starting the Lean server\n\nThe [\\`lean-client-js\\`](https://github.com/leanprover/lean-client-js) package provides a convenient interface for the Lean server; in [a fork](https://github.com/bryangingechen/lean-client-js), I managed to coax webpack to output a UMD module that can be imported with \\`require\\`:\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":2,"value":"lean = require('lean-client-js-browser@2.0.1')","pinned":false,"mode":"js","data":null,"name":null},{"id":1268,"value":"md`The constructor of \\`lean-client-js\\`'s [\\`Server\\` class](https://github.com/leanprover/lean-client-js/blob/master/lean-client-js-core/src/server.ts) takes a \"transport\" object as input. We use a \\`WebWorkerTransport\\` object (which runs the JS/WASM in a WebWorker) (see [code here](https://github.com/bryangingechen/lean-client-js/blob/cache/lean-client-js-browser/src/inprocess.ts)).`","pinned":false,"mode":"js","data":null,"name":null},{"id":5193,"value":"transport = new lean.WebWorkerTransport(transportOpts)","pinned":false,"mode":"js","data":null,"name":null},{"id":1275,"value":"md`The options object passed to the \\`WebWorkerTransport\\` constructor contains URLs to the Lean server's JS/WASM as well as the library ZIP bundle which contains the \\`.olean\\` files (binaries which cache parsed Lean code) for Lean libraries that can then be imported into sessions with the Lean server. For more details, see the [\\`lean-client-js-browser\\` README](https://github.com/leanprover/lean-client-js/tree/master/lean-client-js-browser).\n\nThis notebook uses the core library bundle (~2 MB). It's also possible to load a ZIP bundle containing the main math library for Lean ([\\`mathlib\\`](https://github.com/leanprover-community/mathlib)) by overriding \\`leanLibName\\` with \\`\"library\"\\` when you import from this notebook, but note that it's ~19MB and growing! You can also make ZIP bundles out of your own Lean packages by using the [Python script in the community fork of the \\`lean-web-editor\\`](https://github.com/leanprover-community/lean-web-editor/blob/ui/mk_library.py).\n\n⚠️ \\`lean-client-js\\` caches all ZIP bundles in a IndexedDB store named \"\\`leanlibrary\\`\". This feature is a bit rough and can fail to work if Lean is being initialized in multiple notebooks at the same time (e.g. you may see errors like \"\\`TypeError: First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.\\`\"). Fortunately, reloading seems to fix things. You can avoid this issue by waiting for the Lean editors to appear before opening another notebook. There's also a [button](#deleteIdb) in the [Lean control form](#ServerForm) below which resets this IndexedDB store, in case this cache gets corrupted.\n\nCurrently, the necessary files are hosted by [Scott Morrison](https://tqft.net/) on his webserver (thanks Scott!), though from time to time I may change the host while things update. The JS/WASM files can also be downloaded from the [\\`leanprover-community/lean\\` releases page](https://github.com/leanprover-community/lean-nightly/releases); see [the community fork of the \\`lean-web-editor\\`](https://github.com/leanprover-community/lean-web-editor) for the Python script (\\`mk_library.py\\`) which bundles Lean library packages into a ZIP file.\n\nThe Lean developers are currently hard at work on a new, not-ready-for-public-use, version of Lean ([Lean 4](https://github.com/leanprover/lean4)). Because of this, the Lean server used in this notebook is built from the unofficial [\"community fork\" of Lean 3](https://github.com/leanprover-community/lean/).`","pinned":false,"mode":"js","data":null,"name":null},{"id":3642,"value":"// if you want to use the big bundle of libraries (~19MB rather than ~2MB), change the following to 'library'\nleanLibName = 'libcore'","pinned":false,"mode":"js","data":null,"name":null},{"id":5192,"value":"transportOptsURL = \"https://bryangingechen.github.io/lean/lean-web-editor/\" //'https://gitcdn.link/repo/bryangingechen/lean-web-editor-dist/master/'","pinned":false,"mode":"js","data":null,"name":null},{"id":1049,"value":"transportOpts = {\n  return {\n    javascript: `${transportOptsURL}lean_js_js.js`, // Lean server compiled to asm.js with Emscripten (won't be downloaded or used unless your browser doesn't support WASM)\n    webassemblyJs: `${transportOptsURL}lean_js_wasm.js`, // JS code generated by Emscripten to call...\n    webassemblyWasm: `${transportOptsURL}lean_js_wasm.wasm`, // Lean server compiled to WASM by Emscripten\n    libraryZip: `${transportOptsURL}${leanLibName}.zip`, // bundle of .olean files\n  }\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":3919,"value":"md`We can now create a cell for the server object`","pinned":false,"mode":"js","data":null,"name":null},{"id":6,"value":"server = {\n  __LEAN_SERVER_RESTARTS; // create dependency to be able to restart the server (this is a bit hacky)\n  resetOpenLeanFiles();\n  const server = new lean.Server(transport);\n  server.logMessagesToConsole = logByDefault;\n  server.error.on((err) => console.log('error:', err));\n  // window.server = server; // uncomment this to allow running commands from the dev console\n  server.connect();\n  // return server when it begins to respond to messages\n  return server.search('').then(() => Generators.disposable(server, server => {\n    console.log('disposing lean server...');\n    server.dispose(); // dispose server when cell is invalidated\n  }));\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":5188,"value":"md`and some cells to reset the server. Use \\`resetServer()\\` to re-initialise the Lean server.`","pinned":false,"mode":"js","data":null,"name":null},{"id":5187,"value":"mutable __LEAN_SERVER_RESTARTS = 0","pinned":false,"mode":"js","data":null,"name":null},{"id":5186,"value":"resetServer = () => { mutable __LEAN_SERVER_RESTARTS += 1; }","pinned":false,"mode":"js","data":null,"name":null},{"id":1288,"value":"md`<details><summary>Click here to see the server's main properties</summary>\n\n- The server object has several [\\`Event\\`](https://github.com/leanprover/lean-client-js/blob/master/lean-client-js-core/src/event.ts) channels (\\`error\\`, \\`allMessages\\`, \\`tasks\\`) to which you can attach listener callbacks using \\`event.on()\\`. Each \\`event.on()\\` method returns a \\`handler\\` object with a \\`.dispose()\\` method which can be used to remove the listener from the event. (Note that the \\`tasks\\` event never fires for Lean compiled with Emscripten.)\n- The \\`currentSeqNum\\` property is the ID of the last response received from the Lean server and \\`currentMessages\\` is an array of the error, warning, and informational messages that apply to the files in the server's current [\"range of interest\"](#RangeOfInterest). \n- The \\`sentRequests\\` map consists of requests sent to the server which are still being processed (or which have gotten stuck in some other way). \n- The \\`conn\\` property is the connection to the \\`WebWorkerTransport\\` object. \n`","pinned":false,"mode":"js","data":null,"name":null},{"id":3727,"value":"md`The server object's [main methods](#ServerCommands) will be discussed in the next section.`","pinned":false,"mode":"js","data":null,"name":null},{"id":1291,"value":"md`The following cell controls whether messages to and from the server will be logged to the browser dev console by default. The [\\`leanControl\\` form](#ServerForm) below allows you to change this without restarting the server, so \\`logByDefault\\` is here mainly for use with Observable's [\\`import...with\\` syntax](https://observablehq.com/@observablehq/introduction-to-imports) to make importing the \\`server\\` cell from this notebook a little more configurable.`","pinned":false,"mode":"js","data":null,"name":null},{"id":1133,"value":"logByDefault = false","pinned":false,"mode":"js","data":null,"name":null},{"id":1489,"value":"md`Here's a cell using Observable's [\\`Generators.observe\\`](https://github.com/observablehq/stdlib/#Generators_observe) function that reactively updates whenever the server fires its \\`allMessages\\` event:`","pinned":false,"mode":"js","data":null,"name":null},{"id":205,"value":"leanCurrentMessages = Generators.observe(notify => {\n  const {dispose} = server.allMessages.on(m => notify(m.msgs));\n  notify(server.currentMessages);\n  return dispose;\n}) ","pinned":false,"mode":"js","data":null,"name":null},{"id":1494,"value":"md`The following object, \\`libmeta\\`, contains meta information about the \\`.olean\\` files contained in the \\`.zip\\` file loaded by the server. Its keys are:\n- \\`info\\`: URL prefixes to github repos for the Lean packages (includes the precise commits used)\n- \\`oleanmap\\`: an object whose keys correspond to the olean files and whose values are their corresponding Lean package keys in \\`libmeta.info\\`\n\nThis info is contained in two JSON files generated alongside the library bundle which are used to assist with caching in IndexedDB. They are expected to be in the same directory as the library ZIP file.`","pinned":false,"mode":"js","data":null,"name":null},{"id":1154,"value":"libmeta = {\n  const url = transportOpts.libraryZip.slice(0,-3);\n  return {\n    info: await fetch(`${url}info.json`).then(res => res.json()),\n    oleanmap: await fetch(`${url}olean_map.json`).then(res => res.json()),\n  };\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":3059,"value":"md`You can inspect the imports below; the \\`list\\` field contains an array of names and the \\`tree\\` field contains an object reflecting the directory structure:`","pinned":false,"mode":"js","data":null,"name":null},{"id":3062,"value":"libraryFiles = {\n  const keys = Object.keys(libmeta.oleanmap).map(s => s.replace(/\\//g,'.'));\n  const dir = {};\n  for (let j = 0; j < keys.length; j++) {    \n    const file = keys[j].split('.');\n    let obj = dir;\n    while (file.length > 1) {\n      const part = file.shift();\n      if (typeof obj[part] !== 'object') {\n        obj[part] = (obj[part] !== undefined) ? {'.': obj[part]} : {};\n      }\n      obj = obj[part];\n    }\n    obj[file[0]] = keys[j];\n  }\n  return {list:keys, tree:dir};\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":3070,"value":"md`The following function uses the meta files to get a link to the source file on github corresponding to a given import.`","pinned":false,"mode":"js","data":null,"name":null},{"id":1456,"value":"resolveLeanURL = (file) => `${libmeta.info[libmeta.oleanmap[file]]}${file}.lean`","pinned":false,"mode":"js","data":null,"name":null},{"id":1431,"value":"md`For example, the source \\`.lean\\` file for \\`library/init/data/set.olean\\` in the ZIP file can be found at:`","pinned":false,"mode":"js","data":null,"name":null},{"id":3067,"value":"setLeanURL = resolveLeanURL('init/data/set')","pinned":true,"mode":"js","data":null,"name":null},{"id":1462,"value":"md`We can then fetch the Lean file into an Observable cell:`","pinned":false,"mode":"js","data":null,"name":null},{"id":1465,"value":"setLean = fetch(setLeanURL).then(res => res.text())","pinned":true,"mode":"js","data":null,"name":null},{"id":1459,"value":"md`The Lean server also returns Github links in the \\`source\\` property when responding to \\`server.search\\` and \\`server.info\\` queries.`","pinned":false,"mode":"js","data":null,"name":null},{"id":1245,"value":"Controlling = md`## 2. Controlling the Lean server\n\nIn this section, I'll demonstrate how to interact with the Lean server in Observable.`","pinned":false,"mode":"js","data":null,"name":null},{"id":2611,"value":"ServerForm = md`### Server control form\n\nThe \\`leanControl\\` gadget has several elements that are helpful in debugging and troubleshooting:\n- A checkbox that toggles logging of Lean server messages.\n- A button to Reset \"opened files\", which will be discussed [later in this section](#RangeOfInterest).\n- A button that resets the Lean server.\n- A button to delete the ZIP file cache in your browser's IndexedDB store. (Using this will trigger a redownload of the library bundle; this is only necessary if the cache somehow gets out of sync or becomes corrupted.)\n- A button to delete the cached JS/WASM files. It may be necessary to click this if the version of Emscripten Lean has recently been updated but your browser is still using an old cached version.\n\nThe cells which implement these controls follow. They appear blank because their output HTML elements are referenced in the \\`leanControl\\` form beneath them.\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":621,"value":"serverLogging = {\n  const id = DOM.uid('leanLog').id;\n  // state persists after server reset\n  const check = html`<input id='${id}' type='checkbox' ${(this ? (document.getElementById(id) && document.getElementById(id).checked) : server.logMessagesToConsole) ? 'checked' : ''} />`;\n  const form = html`${check}<label for='${id}'> Log messages to (<code>=></code>) / from (<code><=</code>) server in console`;\n  check.onchange = () => {\n    server.logMessagesToConsole = check.checked;\n    console.log(`Logging ${check.checked ? 'on' : 'off'}!`);\n  };\n  server.logMessagesToConsole = check.checked;\n  return form;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":643,"value":"serverReset = {\n  const button = html`<button title=\"Reset Lean server\">Reset Lean server`;\n  button.onclick = resetServer;\n  return button;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":2763,"value":"deleteIdb = {\n  const b = html`<button title=\"Clear library cache\">Clear library cache`;\n  b.onclick = (e) => {\n    const req = indexedDB.deleteDatabase('leanlibrary');\n    req.onsuccess = () => {\n      console.log('Successfully deleted indexedDB entry \"leanlibrary\".');\n      resetServer();\n    };\n    req.onerror = () => {\n      console.log('Could not delete indexedDB entry \"leanlibrary\".');\n    };\n    req.onblocked = () => {\n      console.log('Could not delete indexedDB entry \"leanlibrary\": operation was blocked.');\n    };\n  };\n  return b;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":3943,"value":"deleteJSWasm = {\n  const b = html`<button title=\"Clear JS/WASM cache\">Clear JS/WASM cache`;\n  b.onclick = (e) => {\n    if (WebAssembly) {\n      fetch(transportOpts.webassemblyJs, {cache: 'reload'})\n        .then(() => fetch(transportOpts.webassemblyWasm, {cache: 'reload'}))\n        .then(() => {\n        console.log('Successfully updated JS & WASM cache.');\n        resetServer();\n      }).catch((e) => console.log(e));\n    } else {\n      fetch(transportOpts.javascript, {cache: 'reload'})\n        .then(() => {\n        console.log('Successfully updated JS cache.');\n        resetServer();\n      }).catch((e) => console.log(e));\n    }\n  };\n  return b;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":1096,"value":"leanControl = html`${serverLogging}<br />\n${roiReset} ${serverReset} ${deleteIdb} ${deleteJSWasm}`","pinned":false,"mode":"js","data":null,"name":null},{"id":1331,"value":"ServerCommands = md`### Server commands\n<details>\n<summary>Click here to see a list of the primary Lean server commands:</summary>\n\nThe main method for communicating with the Lean server is [\\`server.send(req)\\`](https://github.com/leanprover/lean-client-js/blob/83f190d044502d1cd39f320ef15da5357547d539/lean-client-js-core/src/server.ts#L48). Here's its type from that source file:\n\\`\\`\\`js\nsend(req: Request): Promise<CommandResponse> \n\\`\\`\\`\n\nThis sends the given request object [(see the interfaces which extend \\`Request\\` here)](https://github.com/leanprover/lean-client-js/blob/master/lean-client-js-core/src/commands.ts) to the server and returns a promise that resolves when the server sends a response message (see the interfaces extending \\`Response\\` in that link). This class doesn't use the Language Service Protocol, unfortunately, (though there is [a LSP implementation in the \\`lean-client-js\\` repository](https://github.com/leanprover/lean-client-js/tree/master/lean-language-server)).\n\nIt's easier to use the command-specific wrappers for \\`send\\`, which look like this:\n- \\`\\`\\`js\ninfo(file: string, line: number, column: number): Promise<InfoResponse>\n\\`\\`\\`\n  returns type info, docstrings, tactic state at a position in the file, called in [\\`leanEditor()\\`](#leanEditor) to display information about the file at the cursor and when hovering\n\n⚠️ The Lean server works with positions where the line numbers are 1-indexed and column numbers are 0-indexed!\n\n- \\`\\`\\`js\nsync(file: string, contents: string): Promise<CommandResponse>\n\\`\\`\\`\n  sync a Lean file with the server, called in [\\`leanEditor()\\`](#leanEditor) when a file is edited\n\n- \\`\\`\\`js\ncomplete(file: string, line: number, column: number,\n    skipCompletions?: boolean): Promise<CompleteResponse>\n\\`\\`\\`\n  return completion suggestions at a position in the file, used in [\\`leanEditor()\\`](#leanEditor) via CodeMirror's [\"hint\" addon](https://codemirror.net/doc/manual.html#addon_show-hint)\n\n- \\`\\`\\`js\nsearch(query: string): Promise<SearchResponse>\n\\`\\`\\`\n  search the current Lean environment for declarations matching the given string\n\n- \\`\\`\\`js\nallHoleCommands(file: string): Promise<AllHoleCommandsResponse>\n\\`\\`\\`\n  return hole commands for the entire document (Hole commands are info or edit operations that the Lean editor can perform on the source. They are like \"code actions\" in VS Code. [Here's](https://github.com/leanprover-community/mathlib/blob/64431ae841f14108f0d1c4a6c3f519c23e95fb80/docs/holes.md) a very incomplete doc on hole commands provided by mathlib.)\n\n- \\`\\`\\`js\nholeCommands(file: string, line: number, column: number):\n    Promise<HoleCommandsResponse>\n\\`\\`\\`\n  return hole commands for a position in the file, used in [\\`leanEditor()\\`](#leanEditor) via CodeMirror's [\"hint\" addon](https://codemirror.net/doc/manual.html#addon_show-hint)\n\n- \\`\\`\\`js\nhole(file: string, line: number, column: number, action: string):\n    Promise<HoleResponse>\n\\`\\`\\`\n  perform a hole command; returns a message or a replacement (edit) to be applied to the file, used in [\\`leanEditor()\\`](#leanEditor) via CodeMirror's [\"hint\" addon](https://codemirror.net/doc/manual.html#addon_show-hint)\n\n- \\`\\`\\`js\nroi(mode: CheckingMode, files: FileRoi[]): Promise<CommandResponse>\n\\`\\`\\`\n  change the \"range of interest\" of the server, see [below](#RangeOfInterest)\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":3735,"value":"md`These commands can be run in cells like this (although be aware of [Observable's reactivity](https://observablehq.com/@observablehq/how-observable-runs) which may cause them to run multiple times / out-of-order):\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":1334,"value":"// If the server is alive and the results array is empty, try rerunning this cell\nPromises.delay(100, null).then(() => server.search('and'))","pinned":true,"mode":"js","data":null,"name":null},{"id":1355,"value":"md`<details><summary>Click here to see the other methods:</summary>\n\n\n- \\`\\`\\`js\nalive(): boolean\n\\`\\`\\`\n  returns true if the connection to the Lean server is still working\n\n- \\`\\`\\`js\nconnect(): void\n\\`\\`\\`\n  used to initialize the server\n\n- \\`\\`\\`js\ndispose(): void\n\\`\\`\\`\n  used to stop the server\n\n- \\`\\`\\`js\nrestart(): void\n\\`\\`\\`\n  used to restart the server. I haven't figured out quite how to use this yet; the reset button above (which also invalidates all notebook cells that depend on the server object) seems to be more reliable.\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":1315,"value":"RangeOfInterest = md`### Range of interest syncing\n\nThe Lean server can sync with multiple files at once, which means we can \"open\" several different files on a page without having to open new instances of the server.\n\nBy default, the Lean server responds with messages for every file that has been opened since it was started. Currently, there doesn't seem to be a way to actually \"close\" a file in the Lean server. However, we can get it to stop processing and reporting messages on a file by removing the file from the server's \"range of interest\" using [the \\`server.roi()\\` method](https://github.com/leanprover/lean-client-js/blob/master/lean-client-js-core/src/server.ts#L103). \n\nThe \\`vscode-lean\\` extension [uses this feature](https://github.com/leanprover/vscode-lean/blob/master/src/roi.ts) to allow users to specify that Lean processes only \"visible\" lines or \"visible and above\" lines, among several more standard options. In this notebook we use only the \\`'open-files'\\` ROI mode.\n\nThe functions below are used to keep track of the files that are in the server's ROI in a Map object; this is used to ensure that (parts of) Lean files are not being modified by more than one editor or \\`LeanFile\\` object at a time.\n\n⚠️ If \\`openLeanFiles\\` falls out of sync with the editor instances / \\`LeanFile\\` objects on the page, this will result in errors stating that some file is already open when it isn't. Clicking the \\`roiReset\\` button in the [\\`LeanControls\\` form](#LeanControls) above (or calling \\`resetOpenLeanFiles\\') should fix things.\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":1077,"value":"// no output appears here because the button is displayed in leanControl\nroiReset = {\n  const button = html`<button>Reset \"opened files\"`;\n  button.onclick = () => {\n    resetOpenLeanFiles();\n  }\n  return button;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":1024,"value":"roiSync = () => {\n  const fileROIs = Array.from(openLeanFiles.keys()).map(file_name => ({\n    file_name,\n    ranges: [], // empty ranges means whole file\n  }));\n  return server.roi('open-files', fileROIs); // returns a promise\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":5177,"value":"resetOpenLeanFiles = () => {\n  console.log(\"resetting open lean files...\");\n  openLeanFiles.clear();\n  mutable __OPEN_LEAN_FILES_RESETS += 1; \n}","pinned":false,"mode":"js","data":null,"name":null},{"id":5176,"value":"mutable __OPEN_LEAN_FILES_RESETS = 0; // use this as a dependency for modules that should re-run on lean files reset\n// will start at 1 since 'server' calls resetOpenLeanFiles","pinned":false,"mode":"js","data":null,"name":null},{"id":1022,"value":"// keys: filenames, values: Set of indices | true\nopenLeanFiles = new Map()","pinned":false,"mode":"js","data":null,"name":null},{"id":3105,"value":"LeanFileDoc = md`### Lean file objects\n\nThe \\`LeanFile\\` class wraps strings into \"file\" objects that communicate with the Lean server and which are tracked by the \\`openLeanFiles\\` map. This provides a way of working programmatically with the Lean server in JS (in contrast to working with Lean files using CodeMirror via \\`leanEditor\\` in the next section).`","pinned":false,"mode":"js","data":null,"name":null},{"id":3117,"value":"class LeanFile {\n  constructor(opts, invPromise) {\n    __OPEN_LEAN_FILES_RESETS; // re-run when open lean files are reset\n    if (opts.value !== undefined && typeof opts.value !== 'string')\n      throw new Error(`LeanFile: value must be string (currently: ${opts.value}`);\n    if (opts.fileName !== undefined && typeof opts.fileName !== 'string')\n      throw new Error(`LeanFile: fileName must be string (currently: ${opts.fileName}`);\n    if (!(invPromise instanceof Promise))\n      throw new Error(`LeanFile: second argument must be an invalidation promise (currently: ${invPromise})`);\n    \n    const fileName = opts.fileName || DOM.uid('lean').id;\n    if (openLeanFiles.has(fileName)) {\n      throw new Error(`LeanFile: ${fileName} already opened`);\n    }\n    openLeanFiles.set(fileName, true);\n    invPromise.then(() => {\n      openLeanFiles.delete(fileName);\n      roiSync();\n    });\n    \n    Object.defineProperties(this, {\n      opts: {\n        value: opts,\n        enumerable: true,\n      },\n      fileName: {\n        value: fileName,\n        enumerable: true,\n      },\n      _firstSync: {\n        value: false,\n        writable: true,\n      },\n      _hasSynced: {\n        value: () => {\n          if (!this._firstSync)\n            throw new Error(`call the async method '.sync()' to sync the value with the server before working with this object`);\n          if (!server.alive()) {\n            server.error.fire('LeanFile: server is not alive (_hasSynced)');\n            throw new Error(`Server is not alive!`);\n          }\n        },\n      },\n      _updateMsgs: {\n        value: (msgArr) => {\n          this._msgs = msgArr.filter(msg => msg.file_name === this.fileName);\n        },\n      },\n      _value: {\n        writable: true,\n      },\n      _msgs: {\n        writable: true,\n      },\n    });\n    \n    const msgsTimer = new CoalescedTimer();\n    const msgsHandler = server.allMessages.on(({msgs}) => {\n      msgsTimer.do(config.debounceAllMessages, () => {\n        this._updateMsgs(msgs);\n      });\n    });\n    invPromise.then(() => {\n      msgsHandler.dispose();\n    });\n  }\n  \n  get value() {\n    this._hasSynced();\n    return this._value;\n  }\n  set value(v) {\n    throw new Error(`to set the value, use the async method 'sync(value)'`);\n  }\n  sync(v) {\n    addToRunning(this.fileName);\n    return server.sync(this.fileName, v || (this.opts.value || config.defaultValue)).then(\n      () => {\n        removeFromRunning(this.fileName);\n        this._value = v || (this.opts.value || config.defaultValue);\n        if (!this._firstSync) { // add to 'open-files' ROI\n          return roiSync().then(() => {\n            this._firstSync = true;\n            this._updateMsgs(server.currentMessages);\n            return this;\n          });\n        }\n        return this;\n      },\n      (e) => {\n        console.log(`LeanFile ${this.fileName} sync failed: ${e}`);\n        if (!server.alive()) {\n          server.error.fire('LeanFile: server is not alive (sync)');\n          throw new Error(`Server is not alive!`);\n        }\n        return this;\n      }\n    );\n  }\n  \n  get msgs() {\n    this._hasSynced();\n    return this._msgs;\n  }\n  set msgs(m) {\n    throw new Error(`msgs is read-only!`);\n  }\n  \n  info(line, column) {\n    this._hasSynced();\n    return server.info(this.fileName, line, column);\n  }\n  complete(line, column, skipCompletions) {\n    this._hasSynced();\n    return server.complete(this.fileName, line, column, skipCompletions);\n  }\n  allHoleCommands() {\n    this._hasSynced();\n    return server.allHoleCommands(this.fileName);\n  }\n  holeCommands(line, column) {\n    this._hasSynced();\n    return server.holeCommands(this.fileName, line, column);\n  }\n  hole(line, column, action) {\n    this._hasSynced();\n    return server.hole(this.fileName, line, column, action);\n  }\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":3219,"value":"md`The \\`LeanFile\\` constructor has two required parameters:\n- \\`opts\\`: *object*, an options object with two (optional) fields:\n  - \\`fileName\\`: *string*, the filename (a unique filename will be assigned if this is undefined),\n  - \\`value\\`: *string*, the initial value for the file.\n- \\`invPromise\\`: *Promise*, a promise that will resolve when the file should be released. Ordinarily, this should be passed [Observable's \\`invalidation\\` promise](https://observablehq.com/@mbostock/disposing-content).\n\nTo work with a Lean file we must first sync it with the Lean server. However, since communicating with the server is an asynchronous operation, it is inconvenient to sync in the \\`LeanFile\\` constructor. Instead, you can create the \\`LeanFile\\` object and sync it in one line as below:`","pinned":false,"mode":"js","data":null,"name":null},{"id":3154,"value":"testFile = new LeanFile({\n  fileName: 'testLeanFile.lean',\n  value: `#eval 8+8\n#eval [1, 2, 4+4]\n#eval g`,\n}, invalidation).sync()","pinned":true,"mode":"js","data":null,"name":null},{"id":3211,"value":"md`\\`LeanFile\\` objects have the following methods, which are mostly wrappers around the [server methods above](#ServerMethods) where you don't have to provide the filename:\n\n<details><summary>Click here to see a full list of \\`LeanFile\\` methods</summary>\n\n- \\`\\`\\`js\nsync(contents?: string): Promise<LeanFile>\n\\`\\`\\`\n   This method is slightly modified from the server method of the same name to make initialization more convenient. If the contents string is not given, the file will be initialized with \\`opts.value\\` or [\\`config.defaultValue\\`](#ConfigDoc). \n\n- \\`\\`\\`js\ninfo(line: number, column: number): Promise<InfoResponse>\n\\`\\`\\`\n- \\`\\`\\`js\ncomplete(line: number, column: number, \n    skipCompletions?: boolean) : Promise<CommandResponse>\n\\`\\`\\`\n- \\`\\`\\`js\nallHoleCommands(): Promise<AllHoleCommandsResponse>\n\\`\\`\\`\n- \\`\\`\\`js\nholeCommands(line: number, column: number):\n    Promise<HoleCommandsResponse>\n\\`\\`\\`\n- \\`\\`\\`js\nhole(line: number, column: number, action: string):\n    Promise<HoleResponse>\n\\`\\`\\`\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":3183,"value":"// requests info at line 1, column 7\n// (recall that the Lean server works with line numbers that start from 1 and columns that start from 0)\ntestFile.info(1,7)","pinned":true,"mode":"js","data":null,"name":null},{"id":3273,"value":"md`\\`LeanFile\\` objects have the following properties:\n- \\`opts\\`: options used to construct the \\`LeanFile\\` object,\n- \\`msgs\\`: array of messages from the server associated to this Lean file,\n- \\`fileName\\`: the filename,\n- \\`value\\`: current text of the Lean file.`","pinned":false,"mode":"js","data":null,"name":null},{"id":3651,"value":"md`The following function \\`parseLeanMsgs\\` is useful for parsing message arrays from LeanFile objects or Lean editors. It accepts two parameters:\n\n- \\`msgs\\`, an array of objects containing \\`text\\` properties; intended for the \\`msgs\\` array returned by a \\`LeanFile\\` or \\`leanEditor\\` instance; if you're expecting errors, you can pass it a filtered array as below,\n- \\`transform\\`, an (optional) function that is applied to the \\`text\\` property of each object before it is passed to \\`JSON.parse\\`.`","pinned":false,"mode":"js","data":null,"name":null},{"id":3653,"value":"function parseLeanMsgs(msgs, transform = t => t) {\n  if (!msgs) return [];\n  return msgs.map(({text}) => {\n    const t = transform(text);\n    try { // if the eval result is a string encoding a JSON object\n      return JSON.parse(JSON.parse(t));\n    } catch (e) {}\n    try { // if the eval result is an array or number literal\n      return JSON.parse(t);\n    } catch (e) { // return the (transformed) string by default\n      return t;\n    }\n  });\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":3661,"value":"parseLeanMsgs(testFile.msgs.filter(({caption}) => caption === 'eval result'))","pinned":true,"mode":"js","data":null,"name":null},{"id":1248,"value":"CodeMirrorEditor = md`## 3. CodeMirror editor\n\nI learned how to load CodeMirror into Observable and the basics of interacting with it from Tom MacWright's [\"CodeMirror inside of Observable\"](https://observablehq.com/@tmcw/codemirror-inside-of-observable). Note that we have to use an explicit URL in the following \\`require\\` statement since \\`jsdelivr\\` doesn't resolve the \\`/codemirror\\` path as needed by the addons.\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":5174,"value":"CodeMirrorURL = \"https://unpkg.com/codemirror@5/\"","pinned":false,"mode":"js","data":null,"name":null},{"id":24,"value":"CodeMirror = require(`${CodeMirrorURL}lib/codemirror`).then(CodeMirror =>\n  require(`${CodeMirrorURL}addon/hint/show-hint.js`, `${CodeMirrorURL}addon/comment/comment.js`, `${CodeMirrorURL}addon/runmode/runmode.js`).then(\n    () => {\n      initCodeMirrorCSS();\n      initCodeMirrorLeanMode(CodeMirror);\n      initCodeMirrorLeanLint(CodeMirror);\n      return CodeMirror;\n    }\n  )\n)","pinned":false,"mode":"js","data":null,"name":null},{"id":1513,"value":"CMLeanMode = md`### Lean mode\n\nThe following CodeMirror addon provides syntax highlighting for the Lean language. I edited it from [CodeMirror's Haskell mode](https://github.com/codemirror/CodeMirror/blob/master/mode/haskell/haskell.js) and based it on [\\`vscode-lean\\`'s TextMate syntax](https://github.com/leanprover/vscode-lean/blob/master/syntaxes/lean.json).\n\nThe main hack is using \\`state.lastChar\\` to work around the inability to use the \\`\\\\b\\` (word boundary anchor) and \\`(?<!\\\\.)\\` (negative lookbehind for '\\`.\\`') regexes at the beginning of the stream matching functions.\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":5172,"value":"LEAN_MODE = 'lean'","pinned":false,"mode":"js","data":null,"name":null},{"id":5171,"value":"initCodeMirrorLeanMode = CodeMirror => {\n  CodeMirror.defineMode(LEAN_MODE, function(_config, modeConfig) {\n    function switchState(source, setState, state, f) {\n      setState(f);\n      return f(source, setState, state);\n    }\n\n    const digitRE = /\\d/;\n    const hexitRE = /[0-9A-Fa-f]/;\n    const whiteCharRE = /[ \\t\\v\\f]/; // newlines are handled in tokenizer\n    const defBeginRE = /^(inductive|coinductive|structure|theorem|axiom|axioms|abbreviation|lemma|definition|def|instance|class|constant)/;\n    const escapedSingleCharRE = /^'(\\\\\\\\(x[0-9A-Fa-f][0-9A-Fa-f]|u[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]|.))'/;\n    const keywordOtherRE = /^(import|prelude|theory|definition|def|abbreviation|instance|renaming|hiding|exposing|parameter|parameters|begin|constant|constants|lemma|variable|variables|theorem|example|open|axiom|inductive|coinductive|with|structure|universe|universes|alias|precedence|reserve|postfix|prefix|infix|infixl|infixr|notation|end|using|namespace|section|local|set_option|extends|include|omit|class|classes|instances|raw|run_cmd|restate_axiom|calc|have|this|match|do|suffices|show|by|in|at|let|forall|fun|exists|assume|from|λ *|∀ *|Π *|∃ *|Σ *|if|then|else)(?!\\.)\\b/;\n    // const operatorsRE = /^==?|>{0,3}=|>{1,3}|<[\\|$]>|<[\\*=<]?|<<=|≥|≤|!|~|\\?|:=?|←|→|\\$|\\+|\\*|&[&=]?|\\|[\\|=]?/;\n\n    function normal(source, setState, state) {\n      if (source.sol()) state.lastChar = null; // must reset lastChar at start of new line\n\n      if (source.eatWhile(whiteCharRE)) {\n        state.lastChar = source.peek() && ' ';\n        return null;\n      }\n\n      if (source.match(/^--/)) {\n        source.skipToEnd();\n        state.lastChar = null;\n        return 'comment';\n      }\n\n      if (source.match('/-')) {\n        if (source.peek() === '-' || source.peek() === '!') {\n          // doc or module comment\n          source.next();\n          setState(docComment);\n          return 'comment';\n        }\n        return switchState(\n          source,\n          setState,\n          state,\n          ncomment(\"comment\", 1, state)\n        );\n      }\n\n      if (!state.lastChar || !state.lastChar.match(/\\w/)) {\n        // \\b\n        if (source.match(/^(Prop|Type|Sort)\\b/)) {\n          state.lastChar = source.peek() && 'a';\n          return \"builtin\";\n        }\n\n        // \\b\n        if (source.match(/^attribute\\b\\s*\\[[^\\]]*\\]/)) {\n          state.lastChar = source.peek() && ']';\n          return \"attribute\";\n        }\n        // \\b\n        if (source.match(/^@\\[[^\\]]*\\]/)) {\n          state.lastChar = source.peek() && ']';\n          return \"attribute\";\n        }\n\n        // \\b\n        if (source.match(/^sorry\\b/)) {\n          state.lastChar = source.peek() && 'y';\n          return \"error\";\n        }\n\n        if (state.lastChar !== '.') {\n          // \\b no '.'\n          if (\n            source.match(/^(private|meta|mutual|protected|noncomputable)\\b/)\n          ) {\n            state.lastChar = source.peek() && 'a';\n            return \"keyword\";\n          }\n\n          // \\b no '.'\n          if (source.match(defBeginRE)) {\n            setState(defCommand);\n            return \"keyword\";\n          }\n\n          // \\b no '.'\n          if (source.match(keywordOtherRE)) {\n            state.lastChar = source.peek() && 'a';\n            return 'keyword';\n          }\n        }\n      }\n\n      // if (source.match(operatorsRE)) {\n      //   state.lastChar = null;\n      //   return \"operator\";\n      // }\n\n      if (\n        source.match(\n          /^#print\\s+(def|definition|inductive|instance|structure|axiom|axioms|class)\\b/\n        )\n      ) {\n        state.lastChar = source.peek() && 'a';\n        return 'meta';\n      }\n\n      if (source.match(/^#(print|eval|reduce|check|help|exit|find|where)\\b/)) {\n        state.lastChar = source.peek() && 'a';\n        return 'meta';\n      }\n\n      if (source.eat('\"')) {\n        state.lastChar = source.peek() && '\"';\n        return switchState(source, setState, state, stringLiteral);\n      }\n\n      if (source.match(/^'[^\\\\\\\\']'/) || source.match(escapedSingleCharRE)) {\n        state.lastChar = source.peek() && '\\'';\n        return \"string-2\";\n      }\n\n      if (source.match(/^([0-9]+|0([xX][0-9a-fA-F]+))\\b/)) {\n        state.lastChar = source.peek() && '0';\n        return \"number\";\n      }\n\n      const ch = source.next();\n      state.lastChar = ch;\n      return null;\n    }\n\n    function defCommand(source, setState, state) {\n      if (source.eatWhile(whiteCharRE)) return null;\n      if (source.sol()) state.lastChar = null;\n\n      if (source.match(/^--/)) {\n        source.skipToEnd();\n        state.lastChar = null;\n        return 'comment';\n      }\n      if (source.match('/-')) {\n        return switchState(\n          source,\n          setState,\n          state,\n          ncomment(\"comment\", 1, state, defCommand)\n        );\n      }\n\n      if (source.match(/^with\\b|^extends\\b|^[:\\|\\(\\[\\{⦃<>]/, false)) {\n        setState(normal);\n        state.lastChar = source.peek() && 'a';\n        return null;\n      }\n\n      if (!state.lastChar || !state.lastChar.match(/\\w/)) {\n        // \\b\n        if (source.match(/^[^:«»\\(\\)\\{\\} \\t\\v\\f=→λ∀?][^:«»\\(\\)\\{\\} \\t\\v\\f]*/)) {\n          setState(normal);\n          state.lastChar = source.peek() && 'a';\n          return 'def';\n        }\n      }\n      if (source.eat(\"«\")) {\n        state.lastChar = source.peek() && \"»\";\n        return switchState(source, setState, state, defInDoubleAngle);\n      }\n\n      return null;\n    }\n\n    function defInDoubleAngle(source, setState, state) {\n      if (source.eat(\"»\")) {\n        setState(defCommand);\n        return null;\n      }\n\n      source.next();\n      return 'def';\n    }\n\n    function docComment(source, setState, state) {\n      if (source.match('-/')) {\n        setState(normal);\n        state.lastChar = source.peek() && '/';\n        return 'comment';\n      }\n      source.next();\n      return 'comment';\n    }\n\n    function ncomment(type, nest, state, returnState = normal) {\n      if (nest == 0) {\n        state.lastChar = '/';\n        return returnState;\n      }\n      return function(source, setState, state) {\n        let currNest = nest;\n        while (!source.eol()) {\n          const ch = source.next();\n          if (ch == '/' && source.eat('-')) {\n            ++currNest;\n          } else if (ch == '-' && source.eat('/')) {\n            --currNest;\n            if (currNest == 0) {\n              setState(returnState);\n              state.lastChar = source.peek() && '/';\n              return type;\n            }\n          }\n        }\n        setState(ncomment(type, currNest, state, returnState));\n        return type;\n      };\n    }\n\n    function stringLiteral(source, setState, state) {\n      while (!source.eol()) {\n        const ch = source.next();\n        if (ch == '\"') {\n          setState(normal);\n          return \"string\";\n        }\n        if (ch == '\\\\') {\n          if (\n            source.eat('\\\\') ||\n            source.eat('\\\"') ||\n            source.eat('n') ||\n            source.eat('t') ||\n            source.eat('\\'')\n          )\n            return \"string\";\n          else if (source.eat('x')) {\n            source.next(); // should really match only 0-9a-fA-F here and below...\n            source.next();\n            return \"string\";\n          } else if (source.eat('u')) {\n            source.next();\n            source.next();\n            source.next();\n            source.next();\n            return \"string\";\n          }\n        }\n      }\n      setState(normal);\n      return \"string error\";\n    }\n\n    return {\n      startState: function() {\n        return { f: normal, lastChar: null };\n      },\n      copyState: function(s) {\n        return { f: s.f, lastChar: s.lastChar };\n      },\n\n      token: function(stream, state) {\n        // second argument below is \"setState\"; it changes the state function (but not lastChar)\n        return state.f(\n          stream,\n          function(s) {\n            state.f = s;\n          },\n          state\n        );\n      },\n\n      blockCommentStart: \"/-\",\n      blockCommentEnd: \"-/\",\n      lineComment: \"--\"\n    };\n  });\n\n  CodeMirror.defineMIME(\"text/lean\", \"lean\");\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":1518,"value":"md`The \\`leanHL\\` tag function uses CodeMirror's \\`runMode\\` command to provide syntax highlighting outside the editor:`","pinned":false,"mode":"js","data":null,"name":null},{"id":854,"value":"function leanHL() {\n  const pre = html`<pre class=\"cm-s-${CODE_MIRROR_THEME}\" style=\"display: inline\"></pre>`;\n  CodeMirror.runMode(String.raw(...arguments), LEAN_MODE, pre);\n  return pre;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":5169,"value":"md`An alternative formatting function that uses a span for easier formatting possibillities`","pinned":false,"mode":"js","data":null,"name":null},{"id":5168,"value":"function leanHLSpan() {\n  const pre = html`<span class=\"cm-s-${CODE_MIRROR_THEME}\" style=\"display: inline\"></span>`;\n  CodeMirror.runMode(String.raw(...arguments), LEAN_MODE, pre);\n  return pre;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":1633,"value":"md`Snippets of Lean code can be included in markdown blocks, like this:\n\n${leanHL`/- Don't write code like this if you can avoid it! -/\ndef ifun : fin 5 → fin 4 := λ x, if x = 0 then 1 else if x = 1 then 2 else if x = 2 then 3\nelse if x = 3 then 0 else if x = 4 then 1 else 0\n\n#eval ifun 2 -- results in: 3`}`","pinned":false,"mode":"js","data":null,"name":null},{"id":5167,"value":"LeanLintDoc = md`This is the Lean linting addon for CodeMirror; it also includes the hover functionality. It's adapted from [CodeMirror's lint addon](https://github.com/codemirror/CodeMirror/blob/master/addon/lint/lint.js) and [the hover extension in \\`angelozerr/CodeMirror-Extension\\`](https://github.com/angelozerr/CodeMirror-Extension/tree/master/addon/hover) with code copied from [\\`vscode-lean\\`'s hover functionality](https://github.com/angelozerr/CodeMirror-Extension/blob/master/addon/hover/text-hover.js).`","pinned":false,"mode":"js","data":null,"name":null},{"id":5166,"value":"initCodeMirrorLeanLint = CodeMirror => {\n  var GUTTER_ID = \"CodeMirror-lint-markers\";\n\n  function showTooltip(e, content) {\n    var tt = document.createElement(\"div\");\n    tt.className = CSS_CLASSES.CODE_MIRROR_LINT_TOOLTIP;\n    tt.appendChild(content.cloneNode(true));\n    document.body.appendChild(tt);\n\n    function position(e) {\n      if (!tt.parentNode)\n        return CodeMirror.off(document, \"mousemove\", position);\n      tt.style.top = Math.max(0, e.clientY - tt.offsetHeight - 5) + \"px\";\n      tt.style.left = e.clientX + 5 + \"px\";\n    }\n    CodeMirror.on(document, \"mousemove\", position);\n    position(e);\n    if (tt.style.opacity != null) tt.style.opacity = 1;\n    return tt;\n  }\n  function rm(elt) {\n    if (elt.parentNode) elt.parentNode.removeChild(elt);\n  }\n  function hideTooltip(tt) {\n    if (!tt.parentNode) return;\n    if (tt.style.opacity == null) rm(tt);\n    tt.style.opacity = 0;\n    setTimeout(function() {\n      rm(tt);\n    }, 600);\n  }\n\n  // for gutter:\n  function showTooltipFor(e, content, node) {\n    var tooltip = showTooltip(e, content);\n    function hide() {\n      CodeMirror.off(node, \"mouseout\", hide);\n      if (tooltip) {\n        hideTooltip(tooltip);\n        tooltip = null;\n      }\n    }\n    var poll = setInterval(function() {\n      if (tooltip)\n        for (var n = node; ; n = n.parentNode) {\n          if (n && n.nodeType == 11) n = n.host;\n          if (n == document.body) return;\n          if (!n) {\n            hide();\n            break;\n          }\n        }\n      if (!tooltip) return clearInterval(poll);\n    }, 400);\n    CodeMirror.on(node, \"mouseout\", hide);\n  }\n\n  // for onMouseMove:\n  function showTooltipForMove(e, content, node) {\n    var tt = document.createElement(\"div\");\n    tt.className = CSS_CLASSES.CODE_MIRROR_LINT_TOOLTIP;\n    tt.appendChild(content.cloneNode(true));\n    document.body.appendChild(tt);\n    // TODO: don't let tooltip go outside of editor div\n    tt.style.top = Math.max(0, e.clientY - tt.offsetHeight - 5) + \"px\";\n    tt.style.left = e.clientX + 5 + \"px\";\n    if (tt.style.opacity != null) tt.style.opacity = 1;\n    function hide() {\n      CodeMirror.off(document, \"mousemove\", hide);\n      rm(tt);\n    }\n    CodeMirror.on(document, \"mousemove\", hide);\n  }\n\n  function LintState(cm, options, hasGutter) {\n    this.marked = [];\n    this.options = options;\n    this.timeout = null;\n    this.hoverTimeout = null;\n    this.hasGutter = hasGutter;\n    if (options.delay) {\n      this.onMouseMove = function(e) {\n        onMouseMoveWithDelay(cm, e);\n      };\n    } else {\n      this.onMouseMove = function(e) {\n        onMouseMove(cm, e);\n      };\n    }\n    this.waitingFor = 0;\n  }\n\n  function parseOptions(_cm, options) {\n    if (options instanceof Function) return { getAnnotations: options };\n    if (!options || options === true) options = {};\n    return options;\n  }\n\n  function clearMarks(cm) {\n    var state = cm.state.lint;\n    if (state.hasGutter) cm.clearGutter(GUTTER_ID);\n    for (var i = 0; i < state.marked.length; ++i) state.marked[i].clear();\n    state.marked.length = 0;\n  }\n\n  function makeMarker(labels, severity, multiple, tooltips) {\n    var marker = document.createElement(\"div\"),\n      inner = marker;\n    marker.className =\n      \"CodeMirror-lint-marker CodeMirror-lint-marker-\" + severity;\n    if (multiple) {\n      inner = marker.appendChild(document.createElement(\"div\"));\n      inner.className =\n        \"CodeMirror-lint-marker CodeMirror-lint-marker-multiple\";\n    }\n\n    if (tooltips != false)\n      CodeMirror.on(inner, \"mouseover\", function(e) {\n        showTooltipFor(e, labels, inner);\n      });\n\n    return marker;\n  }\n\n  function getMaxSeverity(a, b) {\n    if (a == \"error\") return a;\n    else return b;\n  }\n\n  function groupByLine(annotations) {\n    var lines = [];\n    for (var i = 0; i < annotations.length; ++i) {\n      var ann = annotations[i],\n        line = ann.from.line;\n      (lines[line] || (lines[line] = [])).push(ann);\n    }\n    return lines;\n  }\n\n  function annotationTooltip(ann) {\n    var severity = ann.severity;\n    if (!severity) severity = \"error\";\n    var tip = document.createElement(\"div\");\n    tip.className =\n      \"CodeMirror-lint-message CodeMirror-lint-message-\" + severity;\n    if (typeof ann.messageHTML != 'undefined') {\n      tip.innerHTML = ann.messageHTML;\n    } else if (typeof ann.messageNode != 'undefined') {\n      tip.appendChild(ann.messageNode.cloneNode(true)); // cloneNode added to fix gutter tooltip disappearing after hovering on underlined text (possible bug in CodeMirror?)\n    } else {\n      tip.appendChild(document.createTextNode(ann.message));\n    }\n    return tip;\n  }\n\n  function lintAsync(cm, getAnnotations, passOptions) {\n    var state = cm.state.lint;\n    var id = ++state.waitingFor;\n    function abort() {\n      id = -1;\n      cm.off(\"change\", abort);\n    }\n    cm.on(\"change\", abort);\n    getAnnotations(\n      cm.getValue(),\n      function(annotations, arg2) {\n        cm.off(\"change\", abort);\n        if (state.waitingFor != id) return;\n        if (arg2 && annotations instanceof CodeMirror) annotations = arg2;\n        cm.operation(function() {\n          updateLinting(cm, annotations);\n        });\n      },\n      passOptions,\n      cm\n    );\n  }\n\n  function startLinting(cm) {\n    var state = cm.state.lint,\n      options = state.options;\n    /*\n     * Passing rules in `options` property prevents JSHint (and other linters) from complaining\n     * about unrecognized rules like `onUpdateLinting`, `delay`, `lintOnChange`, etc.\n     */\n    var passOptions = options.options || options;\n    var getAnnotations =\n      options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), \"lint\");\n    if (!getAnnotations) return;\n    if (options.async || getAnnotations.async) {\n      lintAsync(cm, getAnnotations, passOptions);\n    } else {\n      var annotations = getAnnotations(cm.getValue(), passOptions, cm);\n      if (!annotations) return;\n      if (annotations.then)\n        annotations.then(function(issues) {\n          cm.operation(function() {\n            updateLinting(cm, issues);\n          });\n        });\n      else\n        cm.operation(function() {\n          updateLinting(cm, annotations);\n        });\n    }\n  }\n\n  function updateLinting(cm, annotationsNotSorted) {\n    clearMarks(cm);\n    var state = cm.state.lint,\n      options = state.options;\n\n    var annotations = groupByLine(annotationsNotSorted);\n\n    for (var line = 0; line < annotations.length; ++line) {\n      var anns = annotations[line];\n      if (!anns) continue;\n\n      var maxSeverity = null;\n      var tipLabel = state.hasGutter && document.createDocumentFragment();\n\n      // cf. popupTooltips\n      for (var i = 0; i < anns.length; ++i) {\n        var ann = anns[i];\n        var severity = ann.severity;\n        if (!severity) severity = \"error\";\n        maxSeverity = getMaxSeverity(maxSeverity, severity);\n\n        if (options.formatAnnotation) ann = options.formatAnnotation(ann);\n        if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann));\n\n        if (ann.to)\n          state.marked.push(\n            cm.markText(ann.from, ann.to, {\n              className:\n                \"CodeMirror-lint-mark CodeMirror-lint-mark-\" + severity,\n              __annotation: ann\n            })\n          );\n      }\n\n      if (state.hasGutter)\n        cm.setGutterMarker(\n          line,\n          GUTTER_ID,\n          makeMarker(\n            tipLabel,\n            maxSeverity,\n            anns.length > 1,\n            state.options.tooltips\n          )\n        );\n    }\n    if (options.onUpdateLinting)\n      options.onUpdateLinting(annotationsNotSorted, annotations, cm);\n  }\n\n  function popupTooltips(annotations, e) {\n    var target = e.target || e.srcElement;\n    var tooltip = document.createDocumentFragment();\n    for (var i = 0; i < annotations.length; i++) {\n      var ann = annotations[i];\n      tooltip.appendChild(annotationTooltip(ann));\n    }\n    showTooltipForMove(e, tooltip, target);\n  }\n\n  function onMouseMoveWithDelay(cm, e) {\n    var state = cm.state.lint,\n      delay = state.options.delay;\n    clearTimeout(state.hoverTimeout);\n    // if (e.srcElement) {\n    //   // hack for IE, because e.srcElement failed when it is used in the tiemout function\n    //   var newE = {srcElement: e.srcElement, clientX : e.clientX, clientY: e.clientY};\n    //   e = newE;\n    // }\n    state.hoverTimeout = setTimeout(function() {\n      onMouseMove(cm, e);\n    }, delay);\n  }\n\n  function onMouseMove(cm, e) {\n    var state = cm.state.lint;\n    var target = e.target || e.srcElement;\n    var annotations = [];\n    if (/\\bCodeMirror-lint-mark-/.test(target.className)) {\n      var box = target.getBoundingClientRect(),\n        x = (box.left + box.right) / 2,\n        y = (box.top + box.bottom) / 2;\n      var spans = cm.findMarksAt(cm.coordsChar({ left: x, top: y }, \"client\"));\n\n      for (var i = 0; i < spans.length; ++i) {\n        // grab annotation object from gutter marks, created in updateLinting\n        var ann = spans[i].__annotation;\n        if (ann) annotations.push(ann);\n      }\n    }\n    const pos = cm.coordsChar({\n      left: e.clientX,\n      top: e.clientY\n    });\n    if (!pos.outside && Math.abs(pos.xRel) < 10)\n      // see 'hover' function in leanEditor\n      state.options.hoverProvider(cm, pos, e).then(ann => {\n        // prepends the formatted goal object to the annotations if it exists\n        if (ann) annotations.unshift(ann);\n        // unicode symbol input info\n        const symbol = cm.getRange(pos, {\n          line: pos.line,\n          ch: pos.ch + 1\n        });\n        if (symbol) {\n          const symbolAnn = getAbbreviations(symbol);\n          if (symbolAnn) annotations.unshift(symbolAnn);\n        }\n        if (annotations.length) popupTooltips(annotations, e);\n      });\n    // TODO: allow non-async?\n    // if (annotations.length) popupTooltips(annotations, e);\n  }\n\n  // see https://github.com/leanprover/vscode-lean/blob/6678cd653d64dc2c775a72b96900c46baba0829b/src/input.ts#L38\n  function getAbbreviations(symbol) {\n    const abbrevs = [];\n    for (const k in translations) {\n      if (translations[k] === symbol) {\n        abbrevs.push(k);\n      }\n    }\n    return (\n      abbrevs.length > 0 && {\n        severity: 'warning',\n        message: `Input \"${symbol}\" by typing ${abbrevs\n          .sort((a, b) => a.length - b.length)\n          .map(a => '\"\\\\' + a + '\"')\n          .join(' or ')} and then pressing tab.`\n      }\n    );\n  }\n\n  function onChange(cm) {\n    var state = cm.state.lint;\n    if (!state) return;\n    clearTimeout(state.timeout);\n    state.timeout = setTimeout(function() {\n      startLinting(cm);\n    }, state.options.delay || 500);\n  }\n\n  CodeMirror.defineOption(\"lint\", false, function(cm, val, old) {\n    if (old && old != CodeMirror.Init) {\n      clearMarks(cm);\n      if (cm.state.lint.options.lintOnChange !== false)\n        cm.off(\"change\", onChange);\n      CodeMirror.off(\n        cm.getWrapperElement(),\n        \"mouseover\",\n        cm.state.lint.onMouseMove\n      );\n      clearTimeout(cm.state.lint.timeout);\n      delete cm.state.lint;\n    }\n\n    if (val) {\n      var gutters = cm.getOption(\"gutters\"),\n        hasLintGutter = false;\n      for (var i = 0; i < gutters.length; ++i)\n        if (gutters[i] == GUTTER_ID) hasLintGutter = true;\n      var state = (cm.state.lint = new LintState(\n        cm,\n        parseOptions(cm, val),\n        hasLintGutter\n      ));\n      if (state.options.lintOnChange !== false) cm.on(\"change\", onChange);\n      if (state.options.tooltips != false && state.options.tooltips != \"gutter\")\n        CodeMirror.on(cm.getWrapperElement(), \"mousemove\", state.onMouseMove);\n\n      startLinting(cm);\n    }\n  });\n\n  CodeMirror.defineExtension(\"performLint\", function() {\n    if (this.state.lint) startLinting(this);\n  });\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":5325,"value":"md`See also [highlightjs for lean](https://observablehq.com/@bryangingechen/highlightjs-for-lean).`","pinned":false,"mode":"js","data":null,"name":null},{"id":1525,"value":"CMLeanEditor = md`### Lean editor\n\nHere are the main features of the Lean editor:\n- Hovering over text in the editor will display a popup with the following info:\n  - (if hovering over a Unicode character) the relevant [translation strings](#translations),\n  - the type of the term under the mouse pointer and any documentation,\n  - any error, warning or info messages applicable, and\n  - the tactic state at that point (if in tactic mode).\n- Error, warning, and info messages are marked by icons in the gutter on the left and underlined with squiggly lines.\n- By default, the Lean editor shows an infoview panel that has two modes:\n  - <span style='display: inline-block;'>\"Display Goal\" <img src='${displayGoalIcon}'><img src='${displayMsgsIcon}' style='opacity: 0.25;'></span> mode: the type / documentation / tactic state at the editor cursor location is displayed.\n  - <span style='display: inline-block;'> \"Display Messages\" <img src='${displayGoalIcon}' style='opacity: 0.25;'><img src='${displayMsgsIcon}'></span> mode: error, warning, and info messages from the Lean server are displayed.\n- On narrow screens (below ${config.stackWidth}, set by [\\`config.stackWidth\\`](#ConfigDoc)), the built-in infoview panel will be displayed below the editor, rather than on the right.\n- Clicking the Save button <button title='Save .lean file'>Save</button> opens a browser dialog to save the current Lean file to disk. The suggested filename can be changed by changing \\`saveFileName\\` in the editor value object.\n- If the editor is not read-only, a Reset button <button title='Reset editor'>Reset</button> appears. Single-clicking performs a \"soft\" reset, where the value of the editor is replaced with its original value. This value be changed by changing \\`resetValue\\` in the editor value object. Double-clicking performs a \"hard\" reset, which disposes and reinitializes the editor and any [custom header / footer values](#headerFooterDoc).\n\nThe [Codemirror default keybinds](https://github.com/codemirror/CodeMirror/blob/master/src/input/keymap.js) should work in these editors, with a few additions listed here. For non-read-only editors, the \"Toggle toolbar\" button <button title=\"Toggle toolbar\">🛠</button> will show a toolbar containing the buttons below (the toolbar is shown by default if a mobile browser is detected).\n- You can input Unicode characters from [this list](#translations) by typing '\\`\\\\\\`' before its translation string and then pressing Tab or clicking <button title=\"(TAB) translate preceding '\\\\code' to Unicode\">αβγ</button>. For instance, typing '\\`\\\\N\\`' and hitting Tab will result in '\\`ℕ\\`'. Many of the translation strings are LaTeX-inspired, so try those first. You can also hover over a Unicode character to see its translation strings.\n- Completion suggestions pop up automatically after the server syncs, roughly ${config.delayMs}ms after typing stops (this delay is set by [\\`config.delayMs\\`](#ConfigDoc)). They can also be requested manually by typing Ctrl+Space or clicking <button title=\"(CTRL+SPACE) get completions\">…</button>.\n- Type Ctrl+' or click <button title=\"(CTRL+') insert hole\">{!!}</button> to insert the \\`{!!}\\` hole indicator; you can select some text before hitting that key combo and the selection will be placed \"inside\" the hole. Hole commands will be included in the completions if the cursor is by a hole indicator.\n- Type Ctrl+/ or click <button title=\"(CTRL+/) comment line\">--</button> to toggle line comments on a selection.\n- Type Ctrl+m to toggle the display mode of the infoview panel between showing the goal under the cursor (<span style='display: inline-block;'><img src='${displayGoalIcon}'><img src='${displayMsgsIcon}' style='opacity: 0.25;'></span>) and showing all messages (<span style='display: inline-block;'><img src='${displayGoalIcon}' style='opacity: 0.25;'><img src='${displayMsgsIcon}'></span>).\n- Type Esc to enable changing focus off the editor by pressing Tab / Shift+Tab (useful for keyboard navigation). Pressing Esc again before changing focus will restore the usual behavior of Tab / Shift+Tab, as does unfocusing and then refocusing the editor.`","pinned":false,"mode":"js","data":null,"name":null},{"id":3580,"value":"md`The following function creates a Lean editor in an [Observable view](https://observablehq.com/@observablehq/introduction-to-views).\n\nThe other auxiliary functions and cells that it depends on are in [this section](#AuxFunctions).`","pinned":false,"mode":"js","data":null,"name":null},{"id":29,"value":"function leanEditor(opts0, invPromise) {\n  __OPEN_LEAN_FILES_RESETS; // re-run when open lean files are reset\n  const opts = {\n    showToolbar,\n    ...opts0\n  };\n  // -----------------\n  // DOCARRAY CHECKING\n  // -----------------\n  if (\n    opts.docArray &&\n    (!Array.isArray(opts.docArray) || opts.docArray.length === 0)\n  )\n    throw new Error(\n      `leanEditor: docArray must be a non-empty array (currently: ${JSON.stringify(\n        opts.docArray\n      )})`\n    );\n  if (\n    opts.docArray &&\n    (opts.docIndex !== Math.round(opts.docIndex) ||\n      opts.docIndex >= opts.docArray.length ||\n      opts.docIndex < 0)\n  )\n    throw new Error(\n      `leanEditor: docIndex must be an integer between 0 and ${opts.docArray\n        .length - 1} (inclusive) (currently: ${opts.docIndex})`\n    );\n  // Assign opts.value from docArray\n  if (opts.value === undefined)\n    opts.value =\n      (opts.docArray && opts.docArray[opts.docIndex]) || config.defaultValue;\n\n  // --------------------\n  // OTHER INPUT CHECKING\n  // --------------------\n  if (!(invPromise instanceof Promise))\n    throw new Error(\n      `leanEditor: second argument must be an invalidation promise (currently: ${invPromise})`\n    );\n  if (opts.infoView && !(opts.infoView instanceof InfoView))\n    throw new Error(\n      `leanEditor: infoView must be an instance of the InfoView class`\n    );\n\n  // -----------------\n  // FILENAME CHECKING\n  // -----------------\n  const fileName =\n    opts.fileName || DOM.uid('lean').id.replace(/-/g, '_') + '.lean';\n  if (openLeanFiles.has(fileName)) {\n    const val = openLeanFiles.get(fileName);\n    if (val === true || !opts.docArray)\n      throw new Error(`leanEditor: ${fileName} already opened`);\n    if (val.has(opts.docIndex))\n      throw new Error(\n        `leanEditor: index ${opts.docIndex} of ${fileName} already opened`\n      );\n  }\n  // set openLeanFiles here to prevent race condition\n  if (opts.docArray) {\n    const val = openLeanFiles.get(fileName) || new Set([opts.docIndex]);\n    openLeanFiles.set(fileName, val.add(opts.docIndex));\n  } else {\n    openLeanFiles.set(fileName, true);\n  }\n  // ensure roi disposal\n  invPromise.then(() => {\n    if (opts.docArray) {\n      const val = openLeanFiles.get(fileName);\n      if (val) {\n        val.delete(opts.docIndex);\n        if (val.size === 0) {\n          openLeanFiles.delete(fileName);\n        }\n      }\n    } else {\n      openLeanFiles.delete(fileName);\n    }\n    roiSync();\n  });\n\n  // --------------------\n  // EDITOR INSTANCE VARS\n  // --------------------\n  let editInst; // exposed as div.value.editor\n  let filteredMsgs = []; // contains messages in server.currentMessages which apply to this editor instance, set and updated in msgWidget(), exposed as div.value.msgs\n  // the next 3 variables are used in syncIn()\n  const syncTimer = new CoalescedTimer();\n  let thisVersion = 0;\n  let firstSync = false;\n  // The following 3 variables are used to set up a promise which resolves on sync responses:\n  // first used to delay server requests until after first sync (in init())\n  // then used to buffer completion requests (in hint())\n  let syncPromise; // exposed as div.value.syncPromise\n  let syncTrigger;\n  let syncFired = true;\n  // disposal function\n  let dispose;\n  // set to true when server is not alive\n  let dead = false;\n\n  // -------------\n  // HTML ELEMENTS\n  // -------------\n  // the reset and tool buttons are only displayed if not readOnly\n  const saveButton = html`<button title='Save .lean file' class='${CSS_CLASSES.LEAN_EDITOR_SAVE_BUTTON}'>Save</button>`;\n  saveButton.onclick = () => {\n    const file = new Blob(\n      [\n        opts.docArray\n          ? opts.docArray.join(\n              opts.splitString !== undefined ? opts.splitString : '\\n---\\n'\n            )\n          : editInst.getValue()\n      ],\n      { type: 'text/plain' }\n    );\n    const a = document.createElement('a');\n    const url = URL.createObjectURL(file);\n    a.href = url;\n    a.download = div.value.saveFileName;\n    document.body.appendChild(a);\n    a.click();\n    setTimeout(() => {\n      document.body.removeChild(a);\n      window.URL.revokeObjectURL(url);\n    }, 0);\n  };\n  const resetButton = html`<button title='Reset editor' class='${CSS_CLASSES.LEAN_EDITOR_RESET_BUTTON}'>Reset</button>`;\n  // Editing toolbar:\n  const disableReadonly = opts.readOnly ? 'disabled' : '';\n  const commentButton = html`\n    <button title=\"(CTRL+/) comment line\"\n      class=\"${CSS_CLASSES.LEAN_EDITOR_COMMENT_BUTTON}\" ${disableReadonly}>\n      --\n    </button>`;\n  commentButton.onclick = () => {\n    if (!opts.readOnly) {\n      editInst.execCommand('toggleComment');\n    }\n    editInst.focus();\n  };\n  const unicodeButton = html`\n    <button title=\"(TAB) translate preceding '\\\\code' to Unicode\" \n      class=\"${CSS_CLASSES.LEAN_EDITOR_UNICODE_BUTTON}\" ${disableReadonly}>\n      αβγ\n    </button>`;\n  unicodeButton.onclick = () => {\n    if (!opts.readOnly) {\n      unicodeHandler(editInst);\n    }\n    editInst.focus();\n  };\n  const completeButton = html`\n    <button title=\"(CTRL+SPACE) get completions\" class=\"${CSS_CLASSES.LEAN_EDITOR_COMPLETE_BUTTON}\" ${disableReadonly}>\n      …\n    </button>`;\n  completeButton.onclick = () => {\n    if (!opts.readOnly) {\n      editInst.execCommand('autocomplete');\n    }\n    editInst.focus();\n  };\n  const holeButton = html`\n    <button title=\"(CTRL+') insert hole\" class=\"${CSS_CLASSES.LEAN_EDITOR_HOLE_BUTTON}\" ${disableReadonly}>\n      {!!}\n    </button>`;\n  holeButton.onclick = () => {\n    if (!opts.readOnly) {\n      insertHole(editInst);\n    }\n    editInst.focus();\n  };\n  const undoButton = html`\n    <button title=\"(${mac ? 'CMD' : 'CTRL'}+Z) undo\" class=\"${\n    CSS_CLASSES.LEAN_EDITOR_UNDO_BUTTON\n  }\" ${disableReadonly}>\n      ↩️\n    </button>`;\n  undoButton.onclick = () => {\n    if (!opts.readOnly) {\n      editInst.execCommand('undo');\n    }\n    editInst.focus();\n  };\n  const redoButton = html`\n    <button title=\"(${mac ? 'CMD' : 'CTRL'}+SHIFT+Z, ${\n    mac ? 'CMD' : 'CTRL'\n  }+Y) redo\" \n      class=\"${CSS_CLASSES.LEAN_EDITOR_REDO_BUTTON}\" ${disableReadonly}>\n      ↪\n    </button>`;\n  redoButton.onclick = () => {\n    if (!opts.readOnly) {\n      editInst.execCommand('redo');\n    }\n    editInst.focus();\n  };\n  const toolbar = html`\n    <span class='${CSS_CLASSES.LEAN_EDITOR_TOOLBAR}' \n      style='display:${opts.showToolbar ? 'inline' : 'none'};'>\n      ${commentButton}${unicodeButton}${completeButton}${holeButton}${undoButton}${redoButton}\n    </span>`;\n  const toolButton = html`\n    <button class='${CSS_CLASSES.LEAN_EDITOR_TOGGLE_TOOLBAR_BUTTON}' title='Toggle toolbar'>\n      🛠\n    </button>`;\n  toolButton.onclick = () => {\n    toolbar.style.display =\n      toolbar.style.display === 'none' ? 'inline' : 'none';\n    opts.showToolbar = !opts.showToolbar;\n  };\n  // Outer buttons wrapper:\n  const buttonsWrapper = html`\n    <span class='${CSS_CLASSES.LEAN_EDITOR_BUTTON_WRAPPER}'>\n      ${opts.readOnly ? `` : toolbar}\n      ${opts.readOnly ? `` : toolButton}${\n    // only show toolbar if not read-only\n    // only show reset button if not read-only or if there's a header or footer\n    opts.readOnly && !opts.header && !opts.footer ? `` : resetButton\n  }\n      ${saveButton}\n    </span>`;\n  const height = opts.height || config.editorHeight;\n  const cmDiv = html`<div class='${CSS_CLASSES.CODE_MIRROR_CONTAINER}' />`; // CodeMirror is appended to this one!\n  const infoView =\n    opts.infoView ||\n    new InfoView(\n      {\n        displayMode: opts.displayMode,\n        filterMode: opts.filterMode,\n        className: opts.infoViewClassName,\n        node: opts.infoViewNode\n      },\n      invPromise\n    );\n  const containerDiv = html`\n    <div class='${CSS_CLASSES.LEAN_EDITOR_CONTAINER}'>\n      ${cmDiv}${opts.infoView ? `` : infoView.node}\n    </div>`;\n  const header =\n    (typeof opts.header === 'function' ? html`<span />` : opts.header) ||\n    `<span class=\"${CSS_CLASSES.LEAN_EDITOR_HEADER}\">\n       <code>${fileName}</code>\n         ${\n           opts.docArray\n             ? ` (${opts.docIndex + 1}/${opts.docArray.length})`\n             : ``\n         }:\n     </span>`;\n  const footer =\n    (typeof opts.footer === 'function' ? html`<div />` : opts.footer) || ``;\n  const div = html`\n    <div class=\"${`${CSS_CLASSES.LEAN_EDITOR_WRAPPER} ${opts0.className ||\n      ''}`}\">\n      ${header}${buttonsWrapper}${containerDiv}${footer}\n    </div>`;\n  div.value = {};\n  Object.defineProperties(div.value, {\n    opts: {\n      value: opts,\n      enumerable: true\n    },\n    infoView: {\n      value: infoView,\n      enumerable: true\n    },\n    node: {\n      value: div,\n      enumerable: true\n    },\n    editor: {\n      get: () => editInst,\n      enumerable: true\n    },\n    value: {\n      get: () => editInst.getValue(),\n      enumerable: true\n    },\n    resetValue: {\n      value: opts.resetValue || opts.value,\n      enumerable: true,\n      writable: true\n    },\n    syncPromise: {\n      get: () => syncPromise,\n      enumerable: true\n    },\n    fileName: {\n      value: fileName,\n      enumerable: true\n    },\n    saveFileName: {\n      value: fileName,\n      enumerable: true,\n      writable: true\n    },\n    msgs: {\n      get: () => filteredMsgs,\n      enumerable: true\n    },\n    updateInfoView: {\n      value: updateInfoView,\n      enumerable: true\n    }\n  });\n\n  // -------------------------------\n  // SET BACKGROUND WHEN SERVER DIES\n  // -------------------------------\n  const errorHandler = server.error.on(() => {\n    if (!server.alive()) die('errorHandler');\n  });\n  invPromise.then(() => errorHandler.dispose());\n  function die(from) {\n    // only called if !server.alive();\n    if (dead) return;\n    errorHandler.dispose();\n    dead = true;\n    containerDiv.className = `${CSS_CLASSES.LEAN_EDITOR_CONTAINER} ${CSS_CLASSES.STOPPED}`;\n    if (from !== 'errorHandler')\n      server.error.fire(`Server is dead (${from}): ${fileName}`);\n  }\n  if (!server.alive()) die('init');\n\n  // -------------------\n  // FILE SYNC BUFFERING\n  // -------------------\n  function syncIn(ms) {\n    if (syncFired) {\n      syncFired = false;\n      syncPromise = new Promise(resolve => (syncTrigger = resolve));\n      // 'input' event will be dispatched by change()\n    }\n    if (dead) return;\n    addToRunning(fileName); // cf. currentlyRunning cell and runningHandler in init()\n    const version = (thisVersion += 1);\n    syncTimer.do(ms, () => {\n      server.sync(fileName, getDoc(editInst.getValue())).then(\n        () => {\n          if (thisVersion === version) {\n            removeFromRunning(fileName); // cf. currentlyRunning cell and runningHandler in init()\n            syncFired = true;\n            syncTrigger();\n          }\n          if (!firstSync) {\n            // add to 'open-files' ROI\n            roiSync().then(() => (firstSync = true));\n          }\n        },\n        e => {\n          console.log(`error syncing ${fileName} (sync): ${e}`);\n          if (!server.alive()) die('sync');\n        }\n      );\n    });\n  }\n  // function syncNow() { syncIn(0); }\n\n  // ----------------\n  // DOCARRAY HELPERS\n  // ----------------\n  function min_line() {\n    // get first line number of this file (in Lean server)\n    return opts.docArray\n      ? opts.docArray.reduce(\n          (a, d, i) => (i < opts.docIndex ? a + d.split('\\n').length : a),\n          1\n        )\n      : 1;\n  }\n  // CodeMirror's position objects start at line 0 but the displayed line numbers start from line 1\n  // the Lean server's line numbers start at 1\n  opts.lineNumberFormatter = line => min_line() - 1 + line;\n  // getDoc(val) updates and returns the full document if editing part of a docArray,\n  // otherwise it just returns val\n  const getDoc = opts.docArray\n    ? val =>\n        opts.docArray\n          .map((d, i, a) => (i === opts.docIndex ? (a[i] = val) : d))\n          .join('\\n')\n    : d => d;\n\n  // --------------\n  // LINT AND HOVER\n  // --------------\n  // controls gutter messages and underlining\n  function lint(value, options, cm) {\n    if (!filteredMsgs) return null;\n    const lintMsgs = filteredMsgs.map(\n      ({\n        caption,\n        end_pos_col,\n        end_pos_line,\n        pos_col,\n        pos_line,\n        severity,\n        text\n      }) => {\n        const from = {\n          line: pos_line - min_line(),\n          ch: pos_col\n        };\n        return {\n          // replace with commented code when styles / icon for 'information' severity are added\n          severity: severity !== 'error' ? 'warning' : 'error', // severity || 'warning',\n          from,\n          to:\n            end_pos_line !== undefined\n              ? {\n                  line: end_pos_line - min_line(),\n                  ch: end_pos_col\n                }\n              : from, // default to = from\n          // message: (caption ? caption + ':\\n' : '') + text,\n          messageNode: formatMsg({ severity, pos_line, pos_col, caption, text })\n        };\n      }\n    );\n    return lintMsgs;\n  }\n  function hover(cm, data, event) {\n    if (dead) return;\n    const { line, ch } = data;\n    const origLine = line + min_line();\n    return syncPromise\n      .then(() => server.info(fileName, origLine, ch))\n      .then(\n        ({ record }) =>\n          record && {\n            messageNode: formatGoal(record, line, ch),\n            // replace with commented code when styles / icon for 'information' severity are added\n            severity: 'warning' // 'information',\n          },\n        e => {\n          console.log(`error getting info (hover): ${e}`);\n          if (!server.alive()) die('hover');\n        }\n      );\n  }\n  opts.lint = {\n    lintOnChange: true,\n    getAnnotations: lint,\n    hoverProvider: hover,\n    delay: config.hoverDelay\n  };\n\n  // -------------------------\n  // HINT: COMPLETION AND HOLE\n  // -------------------------\n  const hintTimer = new CoalescedTimer();\n  function hint(cm, cb) {\n    if (dead) return;\n    return syncPromise.then(() =>\n      hintTimer.do(\n        0, // only make one completion request when typing ends\n        () => {\n          const { line, ch } = cm.getCursor();\n          const origLine = line + min_line();\n          Promise.all([\n            server.complete(fileName, origLine, ch),\n            server.holeCommands(fileName, origLine, ch)\n          ]).then(\n            ([{ completions, prefix }, { results }]) => {\n              if (!completions && !results) return null;\n              const to = { line, ch };\n              const from = {\n                line: line,\n                ch: ch - ((prefix && prefix.length) || 0)\n              };\n              let list = [];\n              // hole command: apply using hole()\n              if (results)\n                list = list.concat(\n                  results.map(({ name, description }) => ({\n                    displayText: `${name} : ${description}`,\n                    name,\n                    line,\n                    ch,\n                    origLine,\n                    hint: hole\n                  }))\n                );\n              // ordinary completion: display with renderCompletion()\n              if (completions)\n                list = list.concat(\n                  completions.map(({ doc, text, type, tactic_params }) => ({\n                    doc,\n                    text,\n                    type,\n                    tactic_params,\n                    render: renderCompletion\n                  }))\n                );\n              cb({ to, from, list });\n            },\n            e => {\n              console.log(`error getting completions (hint): ${e}`);\n              if (!server.alive()) die('hint');\n            }\n          );\n        }\n      )\n    );\n  }\n  hint.async = true;\n  function hole(cm, self, data) {\n    const { name, line, ch, origLine } = data;\n    if (dead) return;\n    server.hole(fileName, origLine, ch, name).then(\n      ({ message, replacements }) => {\n        if (message) {\n          infoView.hole({ origLine, ch, name, message });\n        }\n        if (replacements && replacements.alternatives) {\n          const {\n            start,\n            end,\n            alternatives: [{ code }]\n          } = replacements;\n          // TODO: ask user if more than one alternative?\n          const from = {\n            line: start.line - min_line(),\n            ch: start.column\n          };\n          const to = {\n            line: end.line - min_line(),\n            ch: end.column\n          };\n          cm.replaceRange(code, from, to, \"complete\");\n          CodeMirror.signal(data, \"pick\", code);\n        }\n      },\n      e => {\n        console.log(`error performing hole command (hole): ${e}`);\n        if (!server.alive()) die('hole');\n      }\n    );\n  }\n  opts.hintOptions = {\n    hint\n    // closeOnUnfocus: false,\n  };\n\n  // ----------------------------\n  // SENDING MESSAGES TO INFOVIEW\n  // ----------------------------\n  const msgFilter = msg =>\n    msg.file_name === fileName &&\n    msg.pos_line >= min_line() &&\n    msg.pos_line < min_line() + editInst.lineCount();\n  function updateMsgs(msgArr) {\n    const msgs = msgArr.filter(msgFilter);\n    const msgsChanged = JSON.stringify(msgs) !== JSON.stringify(filteredMsgs);\n    if (msgsChanged) {\n      filteredMsgs = msgs;\n      div.dispatchEvent(new CustomEvent('input'));\n      editInst.performLint();\n    }\n  }\n  function updateInfoView() {\n    const { line, ch } = editInst.getCursor();\n    const origLine = line + min_line();\n    infoView.update({ fileName, origLine, ch, filteredMsgs, msgFilter });\n  }\n\n  // -----------------\n  // INITIALIZE EDITOR\n  // -----------------\n  function init() {\n    editInst = editor(cmDiv, infoView, opts);\n    editInst.getWrapperElement().style.height = height;\n\n    if (opts.readOnly) {\n      cmDiv.querySelector('.CodeMirror').classList.add(CSS_CLASSES.READONLY);\n      cmDiv\n        .querySelector('.CodeMirror-scroll')\n        .classList.add(CSS_CLASSES.READONLY);\n      cmDiv\n        .querySelector('.CodeMirror-gutters')\n        .classList.add(CSS_CLASSES.READONLY);\n    }\n\n    // set up event handlers\n    function change() {\n      syncIn(config.delayMs);\n      div.dispatchEvent(new CustomEvent('input'));\n    }\n    editInst.on('change', change);\n\n    const msgsTimer = new CoalescedTimer();\n    const msgsHandler = server.allMessages.on(({ msgs }) => {\n      msgsTimer.do(config.debounceAllMessages, () => {\n        editInst.refresh(); // mainly to update displayed line numbers if another part of the docArray is edited\n        updateMsgs(msgs);\n      });\n    });\n\n    editInst.on('cursorActivity', updateInfoView);\n    editInst.on('focus', updateInfoView);\n\n    const runningHandlers = attachRunningHandlers(div.value);\n\n    // ensure event handler disposal\n    dispose = () => {\n      msgsHandler.dispose();\n      runningHandlers.forEach(h => h.dispose());\n    };\n    invPromise.then(dispose);\n\n    // call handlers for the first time\n    change();\n    syncPromise.then(() => {\n      updateMsgs(server.currentMessages);\n      editInst.refresh();\n      updateInfoView();\n    });\n  }\n  let headerNode = null;\n  let footerNode = null;\n  function afterInit() {\n    if (typeof opts.header === 'function') {\n      if (headerNode) header.removeChild(headerNode);\n      headerNode = header.appendChild(opts.header(div.value));\n    }\n    if (typeof opts.footer === 'function') {\n      if (footerNode) footer.removeChild(footerNode);\n      footerNode = footer.appendChild(opts.footer(div.value));\n    }\n    editInst.refresh();\n  }\n\n  // CALL INIT & APPEND HEADER / FOOTER:\n  init();\n  setTimeout(afterInit, 0);\n\n  // RESET BUTTON\n  if (!opts.readOnly) {\n    // different functionality for single click vs double click: https://stackoverflow.com/a/1546183\n    let buttonTimer = null;\n    resetButton.onclick = () => {\n      // double: dispose and rerun init\n      if (buttonTimer) {\n        clearTimeout(buttonTimer);\n        dispose();\n        opts.value = div.value.resetValue;\n        init();\n        setTimeout(afterInit, 0);\n        buttonTimer = null;\n        return;\n      }\n      buttonTimer = setTimeout(() => {\n        // single: set editor value to initial value\n        editInst.setValue(div.value.resetValue);\n        buttonTimer = null;\n      }, config.dblClickDelay);\n    };\n  }\n  return div;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":28,"value":"function editor(container, infoView, opts0) {\n  while (container.firstChild) { container.removeChild(container.firstChild); }\n  const opts = {\n    // defaults: might get overridden by opts0\n    mode: \"lean\",\n    lineWrapping: true,\n    lineNumbers: true,\n    // value should already be set in leanEditor\n    gutters: [\"CodeMirror-lint-markers\"], // required for linting\n    tabIndex: 0,\n    // non-CodeMirror options:\n    // fileName, docArray, and docIndex (used in leanEditor function)\n    // showToolbar\n    // infoView, displayMode, filterMode, infoViewClass, infoViewNode\n    // header, footer (HTML elements placed before / after container in leanEditor)\n    // splitString\n    ...opts0\n  };\n  const editor = CodeMirror(container, opts);\n  \n  // ------------\n  // KEY BINDINGS\n  // ------------\n  function escHandler(cm) {\n    if (!editor.state.completionActive)\n      cm.setOption(\"extraKeys\", navKeyMap);\n  }\n  function navEscHandler(cm) {\n    if (!editor.state.completionActive)\n      cm.setOption(\"extraKeys\", keyMap);\n  }\n  function toggleInfoViewDisplayMode() {\n    infoView.toggleDisplayMode();\n  }\n  const keyMap = {\n    Tab: tabHandler,\n    Esc: escHandler,\n    \"Ctrl-/\": \"toggleComment\",\n    \"Ctrl-'\": insertHole,\n    \"Ctrl-M\": toggleInfoViewDisplayMode,\n  };\n  const navKeyMap = {\n    Tab: false,\n    \"Shift-Tab\": false,\n    Esc: navEscHandler,\n    \"Ctrl-/\": \"toggleComment\",\n    \"Ctrl-'\": insertHole,\n    \"Ctrl-M\": toggleInfoViewDisplayMode,\n  }\n  if (!opts.readOnly) { // enable completion for non-read-only editors\n    navKeyMap[\"Ctrl-Space\"] = keyMap[\"Ctrl-Space\"] = \"autocomplete\";\n    // completion on keyup except for certain characters: https://stackoverflow.com/a/33908969/\n    const keys = {};\n    editor.on(\"keyup\", function(editor, event) {\n      keys[event.key] = false;\n      if (!keys[\"Control\"] &&\n          !editor.state.completionActive &&\n          !ExcludedIntelliSenseTriggerKeys[(event.keyCode || event.which).toString()]) {\n        CodeMirror.commands.autocomplete(editor, null, { completeSingle: false });\n      }\n    });\n    editor.on(\"keydown\", function(editor, event) {\n      keys[event.key] = true;\n    });\n  }\n  editor.setOption(\"extraKeys\", keyMap);\n  editor.on(\"focus\", navEscHandler);\n  return editor;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":2509,"value":"md`The default infoview panels are implemented with the following \\`InfoView\\` class (this can be customized, see [documentation below](#infoViewDoc)).`","pinned":false,"mode":"js","data":null,"name":null},{"id":2248,"value":"class InfoView {\n  constructor(opts, invPromise) {\n    if (!(invPromise instanceof Promise))\n      throw new Error(`InfoView: second argument must be an invalidation promise (currently: ${invPromise})`);\n    \n    Object.defineProperties(this, {\n      opts: {\n        value: opts,\n        enumerable: true,\n      },\n      _displayMode: {\n        value: opts.displayMode || config.displayMode, // 0: goal, 1: all messages\n        writable: true,\n      },\n      _filterMode: {\n        value: opts.filterMode || config.filterMode, // 0: per-editor, 1: per-file\n        writable: true,\n      },\n      _lastGoal: {\n        value: null, // cache data from the last editor that sent info to this infoview\n        writable: true,\n      },\n      _msgFilter: {\n        value: null, // message filtering function either directly provided by the last editor (opts.filterMode === 0) or derived from the filename given by the last editor (opts.filterMode === 1)\n        writable: true,\n      },\n      _filteredMsgs: {\n        value: [],\n        writable: true,\n      },\n      _updateMsgs: {\n        value: (msgArr) => {\n          let msgs = this._msgFilter ? msgArr.filter(this._msgFilter) : [];\n          this._msgsChanged = JSON.stringify(msgs) !== JSON.stringify(this._filteredMsgs);\n          if (this._msgsChanged) {\n            this._filteredMsgs = msgs;\n          }\n        },\n      },\n      _updateTimer: {\n        value: new CoalescedTimer()\n      },\n      _goal: {\n        value: (fileName, origLine, ch) => {\n          if (this._displayMode === 1) {\n            while (this._goalDiv.firstChild) { this._goalDiv.removeChild(this._goalDiv.firstChild); }\n            return; \n          }\n          server.info(fileName, origLine, ch).then(\n            (res) => {\n              while (this._goalDiv.firstChild) { this._goalDiv.removeChild(this._goalDiv.firstChild); }\n              if (res.record) {\n                this._goalDiv.appendChild(formatGoal(res.record, origLine, ch));\n              }\n            },\n            (e) => console.log(`error getting info (InfoView.goal, ${fileName} at ${origLine}:${ch}): ${e}`)\n          );  \n        },\n      },\n      _msgs: {\n        value: (fileName, origLine, ch, msgs) => {\n          while (this._msgsDiv.firstChild) { this._msgsDiv.removeChild(this._msgsDiv.firstChild); }\n          if (this._displayMode === 0) {\n            msgs = msgs.filter(\n              ({pos_col, pos_line, end_pos_col, end_pos_line}) => {\n                return pos_line <= origLine &&\n                  ((!end_pos_line && origLine === pos_line) ||\n                   origLine <= end_pos_line) &&\n                  (origLine !== pos_line || pos_col <= ch) &&\n                  (origLine !== end_pos_line || end_pos_col >= ch);\n              }\n            );\n          }\n          for (const msg of msgs) { this._msgsDiv.appendChild(formatMsg(msg)); }\n        },\n      },\n      _updateIcons: {\n        value: () => {\n          const createClass = (defaultClass, selectedCode) => \n            `${defaultClass} ${this._displayMode === selectedCode ? CSS_CLASSES.ACTIVE : CSS_CLASSES.INACTIVE}`;\n          this._displayGoalImg.className = createClass(CSS_CLASSES.INFO_VIEW_GOAL_ICON, 0);\n          this._displayMsgsImg.className = createClass(CSS_CLASSES.INFO_VIEW_MSGS_ICON, 1);\n        },\n      },\n    });\n    \n    // -------------\n    // HTML ELEMENTS\n    // -------------\n    if (!opts.node) {\n      Object.defineProperties(this, {\n        _displayGoalImg: {\n          value: html`<input type='image' class='${CSS_CLASSES.INFO_VIEW_GOAL_ICON}' src='${displayGoalIcon}' title='Display Goal'/>`,\n        },\n        _displayMsgsImg: { \n          value: html`<input type='image' class='${CSS_CLASSES.INFO_VIEW_MSGS_ICON}' src='${displayMsgsIcon}' title='Display Messages'/>`,\n        },\n        _goalDiv: {\n          value: html`<div class='${CSS_CLASSES.INFO_VIEW_GOAL_ICON}' />`,\n        },\n        _msgsDiv: {\n          value: html`<div class='${CSS_CLASSES.INFO_VIEW_MSGS_ICON}' />`,\n        },\n      });\n      this._updateIcons();\n      Object.defineProperty(this, 'node', {\n        value: html`\n          <div class='${CSS_CLASSES.INFO_VIEW} ${opts.className || ''}'>\n            <div class='${CSS_CLASSES.INFO_VIEW_BUTTONS}'>${this._displayGoalImg}${this._displayMsgsImg}</div>\n            ${this._goalDiv}${this._msgsDiv}\n          </div>`,\n        enumerable: true,\n      });\n    } else {\n      const {node} = opts;\n      if (!(node instanceof Element))\n        throw new Error(`InfoView: opts.node should be an Element (currently: ${node})`);\n      Object.defineProperties(this, {\n        node: {\n          value: node,\n          enumerable: true,\n        },\n        _goalDiv: {\n          value: node.querySelector(`.${CSS_CLASSES.INFO_VIEW_GOAL_WIDGET}`),\n        },\n        _msgsDiv: {\n          value: node.querySelector(`.${CSS_CLASSES.INFO_VIEW_MSGS_WIDGET}`),\n        },\n        _displayGoalImg: {\n          value: node.querySelector(`.${CSS_CLASSES.INFO_VIEW_GOAL_ICON}`),\n        },\n        _displayMsgsImg: {\n          value: node.querySelector(`.${CSS_CLASSES.INFO_VIEW_MSGS_ICON}`),\n        },\n      });\n    }\n    this.node.value = this;\n    \n    this._displayGoalImg.addEventListener('click', () => this.displayModeClick(0));\n    this._displayMsgsImg.addEventListener('click', () => this.displayModeClick(1));\n\n    // --------------\n    // EVENT HANDLERS\n    // --------------\n    Object.defineProperty(this, '_msgsChanged', {\n      value: true, // set and updated in _updateMsgs(), true if filteredMsgs has changed since the last allMessages event\n      writable: true,\n    });\n    const msgsTimer = new CoalescedTimer();\n    const msgsHandler = server.allMessages.on(({msgs}) => {\n      msgsTimer.do(config.debounceAllMessages, () => {\n        this._updateMsgs(msgs);\n        if (this._lastGoal && this._msgsChanged) {\n          const {fileName, origLine, ch} = this._lastGoal;\n          this._goal(fileName, origLine, ch);\n          this._msgs(fileName, origLine, ch, this._filteredMsgs);\n        }\n      });\n    });\n    \n    invPromise.then(() => {\n      msgsHandler.dispose();\n    });\n  }\n  \n  displayModeClick(m) {\n    this._displayMode = m;\n    this._updateIcons();\n    if (this._lastGoal) {\n      const {fileName, origLine, ch} = this._lastGoal;\n      this._goal(fileName, origLine, ch);\n      this._updateMsgs(server.currentMessages);\n      this._msgs(fileName, origLine, ch, this._filteredMsgs);\n    }\n  }\n\n  toggleDisplayMode() {\n    this.displayModeClick(1 - this._displayMode);\n  }\n  \n  // Given filename + cursor position, set filter & last editor info and update infoview\n  update({fileName, origLine, ch, filteredMsgs, msgFilter}) {\n    this._updateTimer.do(config.debounceInfoViewUpdate, () => {\n      this._goal(fileName, origLine, ch);\n      this._lastGoal = {fileName, origLine, ch};\n      if (this._filterMode) {\n        this._msgFilter = (msg) => msg.file_name === fileName;\n        this._updateMsgs(server.currentMessages);\n        this._msgs(fileName, origLine, ch, this._filteredMsgs);\n      } else {\n        this._msgFilter = msgFilter;\n        this._msgs(fileName, origLine, ch, filteredMsgs);\n      }\n    });\n  }\n  \n  hole({origLine, ch, name, message}) {\n    const msg = html`\n      <div class='${CSS_CLASSES.HOLE_MSG_CONTAINER}'>\n        <div class='${CSS_CLASSES.INFO_VIEW_INFO_HEADER}'>${origLine}:${ch} hole command: ${name}</div>\n        <div class='${CSS_CLASSES.INFO_VIEW_CODE_BLOCK}'>${message}</div>\n     </div>`;\n    this._goalDiv.insertBefore(msg, this._goalDiv.firstChild);\n  }\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":1560,"value":"CMLeanEditorUsage = md`### Usage\n\n\\`leanEditor\\` requires two input arguments:\n- \\`opts0\\`, an options object, which can contain any of the [CodeMirror configuration options](https://codemirror.net/doc/manual.html#config). Some useful options there are \\`readOnly\\` and \\`lineNumbers\\`. See the next cell for documentation on the options that control the Lean editor specifically.\n- \\`invPromise\\`, this should usually be given Observable's [\\`invalidation\\` promise](https://observablehq.com/@mbostock/disposing-content) in order for the editor to dispose of its resources properly.\n\n⚠️ The tooltips that pop up on hovering and the completion menus may go outside the editor container. Thus in your notebooks, you should **avoid pinning the source of editor cells or cells immediately before them** since the source displays can obscure the tooltips and menus.`","pinned":false,"mode":"js","data":null,"name":null},{"id":3333,"value":"md`<details><summary>Click here to see a list of the Lean editor-specific options that can be used in \\`opts0\\`:</summary>\n- \\`className\\`: *string*, CSS class name that will be applied to the container div,\n- \\`fileName\\`: *string*, the filename to assign to this editor in the Lean server,\n- \\`height\\`: *dimension*, the height of the editor (e.g. in px or %) (this differs from the way that CodeMirror treats the \\`height\\` option!),\n- \\`showToolbar\\`: *boolean*, show the button toolbar above the editor (by default, is only shown if a mobile browser is detected [below](#showToolbar)),\n- \\`header\\`, \\`footer\\`: *Element* | *function*, Used to add custom DOM elements before / after the editor. Should be either an HTML element or a function of the [CodeMirror editor object](https://codemirror.net/doc/manual.html#api) that returns an HTML element. [Below](#headerFooterDoc), I show how to use this option to display a form which can manipulate the editor.\n- \\`infoView\\`: *InfoView*, if present, the editor will use this [\\`InfoView\\`](#InfoView) object for the info panel. This can be used to share an infoview panel between editors. See an example [below](#infoDivDoc).<br/>If \\`infoView\\` is not set, then the following fields of \\`opts0\\` are passed to the \\`InfoView\\` constructor (discussed before the example [below](#infoViewDoc)):\n  - \\`displayMode\\`: *0* or *1*, (default ${config.displayMode}, controlled by [\\`config.displayMode\\`](#ConfigDoc))\n  - \\`filterMode\\`: *0* or *1*, (default ${config.filterMode}, controlled by [\\`config.filterMode\\`](#ConfigDoc))\n  - \\`infoViewClassName\\`: *string*, placed in the \\`class\\` field of the \\`InfoView\\` options object\n  - \\`infoViewNode\\`: *Element*, placed in the \\`node\\` field of the \\`InfoView\\` options object\n- the \\`value\\` (*string*) and \\`docArray\\` (*Array* of *string*s) options are used to specify the initial text in the editor and are demonstrated in more detail [below](#CMLeanEditorExamples). If \\`docArray\\` is set, the following options may be used:\n  - \\`docIndex\\`: *number* between 0 and \\`docArray.length - 1\\` (inclusive), (required), sets the initial value of the editor to the string at this index in \\`docArray\\`,\n  - \\`splitString\\`: *string* (default \"\\`\\\\n---\\\\n\\`\"). When saving, this string is used to join the parts of the \\`docArray\\`.\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":3337,"value":"md`Here's a minimal invocation of the Lean editor:`","pinned":false,"mode":"js","data":null,"name":null},{"id":1566,"value":"viewof minimalEditor = leanEditor({}, invalidation)","pinned":true,"mode":"js","data":null,"name":null},{"id":1572,"value":"md`When \\`leanEditor\\` is called with an empty options object, a unique filename is automatically generated and some default Lean code is placed in the editor (specified in the global [\\`config\\`](#ConfigDoc) object).\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":2797,"value":"leanEditorValueDoc = md`As pointed out in the intro, Lean editor instances are [views](https://observablehq.com/@observablehq/introduction-to-views). Their values are objects with several fields:\n\n<details>\n- \\`opts\\`*: the options object passed to \\`leanEditor\\`,\n- \\`infoView\\`*: the \\`InfoView\\` object associated to this editor,\n- \\`node\\`*: the DOM node containing the editor and infoview that is returned by \\`leanEditor\\`,\n- \\`editor\\`*: the CodeMirror instance associated to this editor,\n- \\`value\\`*: the string being edited / displayed in the editor (to change the value, use \\`editor.setValue()\\`),\n- \\`resetValue\\`: the string that the editor was initialized with; if the editor is not read-only, the content of the editor will be replaced with this string when the Reset button <button title='Reset editor'>Reset</button> is clicked,\n- \\`syncPromise\\`*: a promise which will resolve the next time the editor finishes syncing with the Lean server,\n- \\`fileName\\`*: the filename given to the Lean server,\n- \\`saveFileName\\`: the filename that is suggested when the Save button <button title='Save .lean file'>Save</button> is clicked,\n- \\`msgs\\`*: an array containing the error, warning, and info message objects that apply to this editor.\n\nThe fields marked with an asterisk (*) are getters and cannot be modified.\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":5208,"value":"md`There's also one method:\n- \\`updateInfoView()\\`: updates the associated \\`InfoView\\` object with the current cursor position and server messages.`","pinned":false,"mode":"js","data":null,"name":null},{"id":3324,"value":"md`Here's the value of the above editor instance:`","pinned":false,"mode":"js","data":null,"name":null},{"id":1858,"value":"minimalEditor","pinned":true,"mode":"js","data":null,"name":null},{"id":1846,"value":"CMLeanEditorExamples = md`### Examples\n- [\\`value\\`](#valueDoc)\n- [\\`docArray\\`, \\`docIndex\\`](#docArrayDoc)\n- [\\`infoView\\`](#infoViewDoc)\n- [\\`header\\`, \\`footer\\`](#headerFooterDoc)\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":2147,"value":"valueDoc = md`#### Edit a Lean file in a single editor\n\nTo do this, use the \\`value\\` field of the options object:`","pinned":false,"mode":"js","data":null,"name":null},{"id":1580,"value":"viewof exampleSingle = leanEditor({\n  fileName: 'comment.lean',\n  value: `-- This is more of a comment than a question, but...`,\n  height: '60px',\n}, invalidation)","pinned":false,"mode":"js","data":null,"name":null},{"id":1588,"value":"docArrayDoc = md`#### Edit a Lean file in multiple editors \n\nTo do this, split the file into an array of strings. Each entry of the string array can then be opened in a single editor instance. The array should be passed to the \\`docArray\\` field of the options object and the array index should be passed to \\`docIndex\\`. \n\nOne handy trick to create such an array is to [split](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split) a string containing a Lean file at certain characters:`","pinned":false,"mode":"js","data":null,"name":null},{"id":1663,"value":"setLeanArray = setLean.split('\\n\\n')","pinned":true,"mode":"js","data":null,"name":null},{"id":149,"value":"viewof setLean0 = leanEditor({\n  docArray: setLeanArray,\n  docIndex: 0, // note that the index starts at 0 and goes to docArray.length - 1\n  fileName: 'set.lean',\n  readOnly: true,\n  height: 'auto',\n}, invalidation)","pinned":false,"mode":"js","data":null,"name":null},{"id":4902,"value":"md`If you need to open many parts of the same \\`docArray\\`, the \\`leanEditorFrom\\` function may be more convenient.`","pinned":false,"mode":"js","data":null,"name":null},{"id":1093,"value":"function leanEditorFrom(opts) {\n  if (!opts.docArray || !opts.fileName)\n    throw new Error(`argument must be an object with 'docArray' and 'fileName' keys (currently: ${JSON.stringify(opts)})`);\n  return function (docIndex, invPromise, extraOpts) { \n    return leanEditor(Object.assign(Object.assign({...opts}, { docIndex }), extraOpts), invPromise);\n  };\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":1617,"value":"md` This function is a \"Lean editor factory factory\". First, call \\`leanEditorFrom\\` with a Lean editor options object (\\`docArray\\` and \\`fileName\\` are required fields) and it will return an editor factory function. In the following text, \\`editorFactory\\` will refer to a generic output of \\`leanEditorFrom\\`.\n\nThe arguments to \\`editorFactory\\` should be:\n- the \\`docIndex\\` (required), \n- the \\`invalidation\\` promise (required), and \n- a Lean editor options object (optional). \n\nThe output of \\`editorFactory\\` is a Lean editor that opens the part of \\`docArray\\` specified with \\`docIndex\\` and which uses the options object passed to \\`leanEditorFrom\\`. The options passed to \\`editorFactory\\` override those passed to \\`leanEditorFrom\\`.\n\nHere's a typical example usage of \\`leanEditorFrom\\`. We first create an editor factory function named \\`setLeanEditor\\`:`","pinned":false,"mode":"js","data":null,"name":null},{"id":1776,"value":"setLeanEditor = leanEditorFrom({\n  docArray: setLeanArray,\n  fileName: 'set.lean',\n  readOnly: true,\n  height: '60px',\n})","pinned":true,"mode":"js","data":null,"name":null},{"id":4896,"value":"md`Now we can create more editor views of this file with \\`setLeanEditor\\`:`","pinned":false,"mode":"js","data":null,"name":null},{"id":1779,"value":"setLeanEditor(10, invalidation)","pinned":true,"mode":"js","data":null,"name":null},{"id":905,"value":"md`**Note the following**:\n\n⚠️ Edits made to the document will mutate the entries of \\`docArray\\`! Also, if you give both a \\`value\\` and \\`docArray\\`, the \\`value\\` string will override the entry of \\`docArray\\` when the editor is initialized / reset.\n\nFor now, opening the same file or file part in multiple editors is not supported.`","pinned":false,"mode":"js","data":null,"name":null},{"id":1792,"value":"viewof set2a = setLeanEditor(2, invalidation)","pinned":true,"mode":"js","data":null,"name":null},{"id":1794,"value":"viewof set2b = setLeanEditor(2, invalidation)","pinned":true,"mode":"js","data":null,"name":null},{"id":2120,"value":"infoViewDoc = md`#### Share an infoview panel between editors\n\nYou can use the \\`infoView\\` option to specify a custom HTML element to use for the infoview. If this option is used, the Lean editor will send its messages to the given [\\`InfoView\\` object](#infoViewDoc). To create such an object, call the constructor with an options object and the \\`invalidation\\` promise. \n\n<details><summary>Click to see a list of the options:</summary>\n- \\`displayMode\\`: *0* or *1*, (default ${config.displayMode}, controlled by [\\`config.displayMode\\`](#ConfigDoc)). If set to 0, this infoview panel displays the goal under the cursor when it starts (<span style='display: inline-block;'><img src='${displayGoalIcon}'><img src='${displayMsgsIcon}' style='opacity: 0.25;'></span>). If set to 1, it displays all messages when it starts (<span style='display: inline-block;'><img src='${displayGoalIcon}' style='opacity: 0.25;'><img src='${displayMsgsIcon}'></span>).\n- \\`filterMode\\`: *0* or *1*, (default ${config.filterMode}, controlled by [\\`config.filterMode\\`](#ConfigDoc)). Changes the messages shown when this infoview panel is set to display messages (i.e. when \\`displayMode\\` is 1 <span style='display: inline-block;'><img src='${displayGoalIcon}' style='opacity: 0.25;'><img src='${displayMsgsIcon}'></span>). An infoview panel will always display messages from the last editor that sent it data. If set to 0, only messages associated to the range of lines displayed in that editor are shown. If set to 1, all messages associated to the file opened in that editor are shown.\n- \\`className\\`: *string*, a custom CSS class name applied to the infoview panel. By default, the class is \\`infoview\\`. See [\\`infoViewCSS\\`](#infoViewCSS) for the relevant style rules.\n- \\`node\\`: *Element*, an HTML element to use in place of the default one. To function properly, the element should have children with classes \\`${CSS_CLASSES.INFO_VIEW_GOAL_ICON}\\`, \\`${CSS_CLASSES.INFO_VIEW_MSGS_ICON}\\`, \\`${CSS_CLASSES.INFO_VIEW_GOAL_WIDGET}\\`, and \\`${CSS_CLASSES.INFO_VIEW_MSGS_WIDGET}\\`.\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":3814,"value":"md`\\`InfoView\\` objects have two public methods:\n- \\`displayModeClick(i)\\`: changes the infoview display mode to \\`i\\` (as above, 0 means display goal <span style='display: inline-block;'><img src='${displayGoalIcon}'><img src='${displayMsgsIcon}' style='opacity: 0.25;'></span>, 1 means display messages <span style='display: inline-block;'><img src='${displayGoalIcon}' style='opacity: 0.25;'><img src='${displayMsgsIcon}'></span>.)\n- \\`toggleDisplayMode()\\`: toggles the infoview display mode between 0 and 1.\n\nAnd two public properties:\n- \\`node\\`: the HTML element for the infoview panel\n- \\`opts\\`: the options that were used to create this object\n\nThere are also two methods used by \\`leanEditor\\` to send data to the \\`InfoView\\` object which probably should not be called otherwise:\n<details>\n\n- \\`update({fileName, origLine, ch, filterMsgs, msgFilter})\\`: used to pass data about the cursor location (\\`origLine\\` (line number in the Lean file), \\`ch\\`) and the messages associated to the lines in the editor (\\`filteredMsgs\\`). When \\`filterMode\\` is 0, \\`msgFilter\\` is used as the filter for selecting from the server's messages.\n- \\`hole({origLine, ch, name, message})\\`: used to display a hole message in the infoview.\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":5213,"value":"md`Also relevant: the \\`.updateInfoView()\\` method of [a Lean editor's \\`value\\` object](#leanEditorValueDoc) updates its linked \\`InfoView\\` object with the editor's cursor position and the relevant server messages. `","pinned":false,"mode":"js","data":null,"name":null},{"id":3328,"value":"md`The following cells demonstrate how one can share an infoview between multiple editors:`","pinned":false,"mode":"js","data":null,"name":null},{"id":2060,"value":"style2 = html`<style>\n.${CSS_CLASSES.INFO_VIEW}.info-view-2 {\n  margin: 0;\n  height: 150px;\n}\n</style>`","pinned":false,"mode":"js","data":null,"name":null},{"id":2640,"value":"viewof sharedInfoview = style2, new InfoView({className: 'info-view-2'}, invalidation).node","pinned":false,"mode":"js","data":null,"name":null},{"id":3128,"value":"sharedInfoview","pinned":false,"mode":"js","data":null,"name":null},{"id":2133,"value":"md`Try clicking between the following editors:`","pinned":false,"mode":"js","data":null,"name":null},{"id":2072,"value":"leanEditor({\n  fileName: 'printeq.lean',\n  infoView: sharedInfoview,\n  value: `#print eq`,\n  height: '30px',\n  lineNumbers: false,\n}, invalidation)","pinned":false,"mode":"js","data":null,"name":null},{"id":2083,"value":"leanEditor({\n  fileName: 'printplus.lean',\n  infoView: sharedInfoview,\n  value: `#print +`,\n  height: '30px',\n  lineNumbers: false,\n}, invalidation)","pinned":false,"mode":"js","data":null,"name":null},{"id":2721,"value":"md`See also the [Lean exercise box](https://observablehq.com/@bryangingechen/lean-exercise-box) notebook which shows two editors sharing an infoview in a more compact layout. \n\nThis feature is inspired by [this page](https://www.math.u-psud.fr/~pmassot/lean/) generated by Patrick Massot's [\\`format_lean\\`](https://github.com/leanprover-community/format_lean); unfortunately, [fixed-position divs are not supported on \\`observablehq.com\\`](https://talk.observablehq.com/t/sticky-div-and-top-button-integration-to-notebook/1079/2), though it should be possible to put together a nice-looking inspector along these lines for exported notebooks...\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":2156,"value":"headerFooterDoc = md`#### Add a header and / or footer to the editor\n\nThe \\`header\\` and \\`footer\\` options can be used to add HTML elements to the editor. They can be set to either an HTML element or a function of one argument (the [value of the \\`leanEditor\\`](#leanEditorValueDoc)) that returns an HTML element.\n\nThe most useful field in the object passed to a header / footer function is \\`editor\\` (containing the CodeMirror instance).\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":3395,"value":"md`Here's a simple example showing how to hide the default header:`","pinned":false,"mode":"js","data":null,"name":null},{"id":3391,"value":"leanEditor({\n  fileName: 'test2.lean',\n  value: `example : 3 * 12 - 4 = 32 := rfl`,\n  header: html``,\n  height: '100px'\n}, invalidation)","pinned":false,"mode":"js","data":null,"name":null},{"id":3614,"value":"md`For more examples see [Lean exercise box](https://observablehq.com/@bryangingechen/lean-exercise-box) (simple) and [Stepping through currying in Lean](https://observablehq.com/@bryangingechen/stepping-through-currying-in-lean) (fancy).`","pinned":false,"mode":"js","data":null,"name":null},{"id":1556,"value":"ConfigDoc = md`### Global config object\n\nUnless otherwise specified in the comments, these global options cannot be overridden per editor instance. If you wish to override \\`config\\` using \\`import...with\\`, note that all the fields included below are required!\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":938,"value":"config = ({\n  // default editor value; CAN OVERRIDE with 'value' in leanEditor options\n  defaultValue: `-- Live ${\n    WebAssembly ? 'WebAssembly' : 'JavaScript'\n  } version of Lean (default Lean file):\n#eval let v := lean.version in let s := lean.special_version_desc in string.join\n[\"Lean (version \", v.1.repr, \".\", v.2.1.repr, \".\", v.2.2.repr, \", \",\nif s ≠ \"\" then s ++ \", \" else s, \"commit \", (lean.githash.to_list.take 12).as_string, \")\"]\n\nexample (m n : ℕ) : m + n = n + m :=\nby simp [nat.add_comm]`,\n  // default InfoView display mode; CAN OVERRIDE with 'displayMode' in leanEditor options\n  displayMode: 0, // 0: goal, 1: messages\n  // default InfoView message filter mode; CAN OVERRIDE with 'filterMode' in leanEditor options\n  // in \"display messages\" mode, show all messages associated with:\n  filterMode: 0, // 0: the lines in the currently active editor, 1: the fileName in the currently active editor\n  // STYLING\n  editorHeight: '250px', // default height; CAN OVERRIDE with 'height' in leanEditor options\n  editorPercentage: 0.65, // percentage of width / height of editor vs InfoView (also changes CSS)\n  stackWidth: '720px', // min width of window before changing from vertical to horizontal panel arrangement (changes leanCSS also)\n  runningColor: '#fa5', // editor outline while server is running: orange-ish\n  doneColor: '#bfb', // editor outline when server is done: green-ish\n  initColor: '#aff', // editor outline before it finishes initializing: blue-ish\n  stoppedColor: '#faa', // editor outline if server has stopped: pink-ish\n  // TIMING (in milliseconds)\n  delayMs: 750, // delay after last typed character before syncing editor contents with server\n  hoverDelay: 400, // delay before popping up a hover element\n  debounceAllMessages: 150, // delay after last \"allMessages\" event before updating InfoView\n  debounceInfoViewUpdate: 100, // delay after last request to update goal before making a server.info() request and updating InfoView\n  dblClickDelay: 250 // speed of double-click on reset button\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":1813,"value":"AuxFunctions = md`### Auxiliary functions\n\nThe cells in this section are used in [\\`leanEditor\\`](#leanEditor), [\\`editor\\`](#editor) and [\\`InfoView\\`](#InfoView). Brief descriptions follow.\n\nThis group of functions is used to format messages in the infoview panel; they are more or less copied from React components in [\\`lean-web-editor\\`](https://github.com/leanprover/lean-web-editor):`","pinned":false,"mode":"js","data":null,"name":null},{"id":363,"value":"function formatGoal(goal, line, ch) {\n  const {text, doc, type, state} = goal;\n  const goalDiv = html`<div class='msg-container'>`;\n  if (state) {\n    goalDiv.appendChild(html`<div class='${CSS_CLASSES.INFO_VIEW_INFO_HEADER}'>${line}:${ch}: goal</div>`);\n    goalDiv.appendChild(html`<div class='${CSS_CLASSES.INFO_VIEW_CODE_BLOCK}'>${leanHLSpan`${state}`}</div>`);\n  }\n  if (text) {\n    goalDiv.appendChild(html`\n      <div class='${CSS_CLASSES.INFO_VIEW_INFO_HEADER}'>\n        tactic <span class='${CSS_CLASSES.INFO_VIEW_CODE_BLOCK}'>${leanHLSpan`${text}`}</span>\n      </div>`);\n  } else if (type) {\n    goalDiv.appendChild(html`\n      <div class='${CSS_CLASSES.INFO_VIEW_INFO_HEADER}'>\n        ${line}:${ch}: type ${goal['full-id'] ? `of <span class='${CSS_CLASSES.INFO_VIEW_CODE_BLOCK}'>${goal['full-id']}</span>` : ``}\n      </div>`);\n    goalDiv.appendChild(html`<div class='${CSS_CLASSES.INFO_VIEW_CODE_BLOCK}'>${leanHLSpan`${type}`}</div>`);\n  }\n  if (doc) {\n    goalDiv.appendChild(formatDoc(doc));\n  }\n  return goalDiv;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":427,"value":"function formatDoc(doc) {\n  let showDoc = doc.length < 80;\n  function docVal() {\n    return showDoc ? md`${doc}` : md`${doc.slice(0,75)}<span style='color: #246;'>[...]</span>`;\n  }\n  const div = html`<div class='${CSS_CLASSES.INFO_VIEW_TOGGLE_DOC}'>${docVal()}`;\n  div.onclick = () => {\n    showDoc = !showDoc;\n    while (div.firstChild) { div.removeChild(div.firstChild); }\n   div.appendChild(docVal());\n  };\n  return div;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":345,"value":"function formatMsg(msg) {\n  const {severity, pos_line, pos_col, caption, text} = msg;\n  return html`\n    <div class='msg-container'>\n      <div class='${CSS_CLASSES.INFO_VIEW_INFO_HEADER}' style='color: ${colorOfSeverity[severity]}'>${pos_line}:${pos_col} ${severity}: ${caption}</div>\n      <div class='${CSS_CLASSES.INFO_VIEW_CODE_BLOCK}'>${leanHLSpan`${text}`}</div>\n    </div>`;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":346,"value":"colorOfSeverity = ({\n  information: 'green',\n  warning: 'orange',\n  error: 'red',\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":2518,"value":"md`These infoview panel icons for switching between showing the goal <img src=\"${displayGoalIcon}\"> and showing all messages <img src=\"${displayMsgsIcon}\"> are taken from [\\`vscode-lean\\`](https://github.com/leanprover/vscode-lean):`","pinned":false,"mode":"js","data":null,"name":null},{"id":1982,"value":"displayGoalIcon = `data:image/svg+xml;utf8,${encodeURIComponent(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"18\" height=\"18\" viewBox=\"0 0 18 18\">\n  <g style=\"fill:#000000\">\n    <path d=\"M 1 1 V 5 L 5.5 3.5 Z\" />\n    <rect x=\"5\" width=\"12\" y=\"3\" height=\"5\" />\n    <rect x=\"5\" width=\"8\" y=\"9\" height=\"2\" />\n    <rect x=\"5\" width=\"10\" y=\"12\" height=\"2\" />\n    <rect x=\"5\" width=\"9\" y=\"15\" height=\"2\" />\n  </g>\n</svg>`)}`","pinned":false,"mode":"js","data":null,"name":null},{"id":1986,"value":"displayMsgsIcon = `data:image/svg+xml;utf8,${encodeURIComponent(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"18\" height=\"18\" viewBox=\"0 0 18 18\">\n  <g style=\"fill:#000000\">\n    <path d=\"M 1 1 V 5 L 5.5 3.5 Z\" />\n    <rect x=\"3\" width=\"14\" y=\"6\" height=\"2\" />\n    <rect x=\"3\" width=\"14\" y=\"9\" height=\"2\" />\n    <rect x=\"3\" width=\"14\" y=\"12\" height=\"2\" />\n    <rect x=\"3\" width=\"14\" y=\"15\" height=\"2\" />\n  </g>\n</svg>`)}`","pinned":false,"mode":"js","data":null,"name":null},{"id":3490,"value":"md`This mutable variable uses a nasty regex to detect whether you're on mobile. If you are, then the editor will expand the toolbar by default.`","pinned":false,"mode":"js","data":null,"name":null},{"id":3487,"value":"// http://detectmobilebrowser.com/mobile (UNLICENSE)\nshowToolbar = {\n  const a = (navigator.userAgent||navigator.vendor||window.opera);\n  return (/(android|bb\\d+|meego|android|ipad|playbook|silk).+mobile|avantgo|bada\\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\\-(n|u)|c55\\/|capi|ccwa|cdm\\-|cell|chtm|cldc|cmd\\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\\-s|devi|dica|dmob|do(c|p)o|ds(12|\\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\\-|_)|g1 u|g560|gene|gf\\-5|g\\-mo|go(\\.w|od)|gr(ad|un)|haie|hcit|hd\\-(m|p|t)|hei\\-|hi(pt|ta)|hp( i|ip)|hs\\-c|ht(c(\\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\\-(20|go|ma)|i230|iac( |\\-|\\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\\/)|klon|kpt |kwc\\-|kyo(c|k)|le(no|xi)|lg( g|\\/(k|l|u)|50|54|\\-[a-w])|libw|lynx|m1\\-w|m3ga|m50\\/|ma(te|ui|xo)|mc(01|21|ca)|m\\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\\-2|po(ck|rt|se)|prox|psio|pt\\-g|qa\\-a|qc(07|12|21|32|60|\\-[2-7]|i\\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\\-|oo|p\\-)|sdk\\/|se(c(\\-|0|1)|47|mc|nd|ri)|sgh\\-|shar|sie(\\-|m)|sk\\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\\-|v\\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\\-|tdg\\-|tel(i|m)|tim\\-|t\\-mo|to(pl|sh)|ts(70|m\\-|m3|m5)|tx\\-9|up(\\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\\-|your|zeto|zte\\-/i.test(a.substr(0,4)));\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":4926,"value":"md`The following cell detects whether you're on a mac in order to display the proper undo/redo keyboard shortcuts in the toolbar tooltips.`","pinned":false,"mode":"js","data":null,"name":null},{"id":4923,"value":"mac = /Mac|iPhone|iPad/.test(navigator.platform); // cf. https://observablehq.com/@mbostock/keys","pinned":false,"mode":"js","data":null,"name":null},{"id":1821,"value":"md`The \\`currentlyRunning\\` object of the \\`ReactiveValue\\` class is used to keep track of when the server is busy with a sync request from a LeanFile object or Lean editor instance. (this code is from the [\\`lean-web-editor\\`](https://github.com/leanprover/lean-web-editor/blob/master/src/langservice.ts))`","pinned":false,"mode":"js","data":null,"name":null},{"id":193,"value":"currentlyRunning = server, new ReactiveValue([])","pinned":false,"mode":"js","data":null,"name":null},{"id":188,"value":"function addToRunning(fn) {\n  if (currentlyRunning.value.indexOf(fn) === -1) {\n    currentlyRunning.updated.fire([].concat([fn], currentlyRunning.value));\n  }\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":190,"value":"function removeFromRunning(fn) {\n  currentlyRunning.updated.fire(currentlyRunning.value.filter((v) => v !== fn));\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":179,"value":"class ReactiveValue {\n  constructor(initialValue) {\n    this.updated = new lean.Event();\n    this.lastValue = initialValue;\n    this.updated.on((e) => this.lastValue = e);\n  }\n\n  get value() { return this.lastValue; }\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":4972,"value":"md`This function is used by the Lean editor to change the border color between <span style='padding: 1px; border-style: solid; border-color: ${config.runningColor};'>busy</span> and <span style='padding: 1px; border-style: solid; border-color:  ${config.doneColor}'>done</span> states.`","pinned":false,"mode":"js","data":null,"name":null},{"id":4957,"value":"function attachRunningHandlers({node}) {\n  const containerDiv = node.querySelector(`.${CSS_CLASSES.LEAN_EDITOR_CONTAINER}`);\n  return [currentlyRunning.updated.on((files) => {\n      containerDiv.className = `${CSS_CLASSES.LEAN_EDITOR_CONTAINER} ${files.length ? CSS_CLASSES.RUNNING : CSS_CLASSES.DONE}`;\n    })];\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":1824,"value":"md`The \\`CoalescedTimer\\` class ([taken from \\`lean-web-editor\\`](https://github.com/leanprover/lean-web-editor/blob/8922763b22177adccf9d59e39eda504ee7680fe1/src/langservice.ts)) (building off of [\\`Event\\` from \\`lean-client-js\\`](https://github.com/leanprover/lean-client-js/blob/master/lean-client-js-core/src/event.ts)) is used in \\`leanEditor\\` for debouncing and batching events: e.g. delaying syncing until after [\\`config.delayMs\\`](#ConfigDoc) milliseconds without any typing.`","pinned":false,"mode":"js","data":null,"name":null},{"id":177,"value":"class CoalescedTimer {\n  do(ms, f) {\n    if (this.timer) {\n      clearTimeout(this.timer);\n    }\n    this.timer = setTimeout(() => {\n      this.timer = undefined;\n      f();\n    }, ms);\n  }\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":1830,"value":"md`These objects are used for the autocompletion and Unicode translation features.`","pinned":false,"mode":"js","data":null,"name":null},{"id":718,"value":"// https://stackoverflow.com/a/33908969/\nExcludedIntelliSenseTriggerKeys =\n  ({\n  \"8\": \"backspace\",\n  \"9\": \"tab\",\n  \"13\": \"enter\",\n  \"16\": \"shift\",\n  \"17\": \"ctrl\",\n  \"18\": \"alt\",\n  \"19\": \"pause\",\n  \"20\": \"capslock\",\n  \"27\": \"escape\",\n  \"32\": \"space\",\n  \"33\": \"pageup\",\n  \"34\": \"pagedown\",\n  \"35\": \"end\",\n  \"36\": \"home\",\n  \"37\": \"left\",\n  \"38\": \"up\",\n  \"39\": \"right\",\n  \"40\": \"down\",\n  \"45\": \"insert\",\n  \"46\": \"delete\",\n  \"91\": \"left window key\",\n  \"92\": \"right window key\",\n  \"93\": \"select\",\n  \"107\": \"add\",\n  \"109\": \"subtract\",\n  \"110\": \"decimal point\",\n  \"111\": \"divide\",\n  \"112\": \"f1\",\n  \"113\": \"f2\",\n  \"114\": \"f3\",\n  \"115\": \"f4\",\n  \"116\": \"f5\",\n  \"117\": \"f6\",\n  \"118\": \"f7\",\n  \"119\": \"f8\",\n  \"120\": \"f9\",\n  \"121\": \"f10\",\n  \"122\": \"f11\",\n  \"123\": \"f12\",\n  \"144\": \"numlock\",\n  \"145\": \"scrolllock\",\n  \"186\": \"semicolon\",\n  \"187\": \"equalsign\",\n  \"188\": \"comma\",\n  \"189\": \"dash\",\n  // \"190\": \"period\",\n  \"191\": \"slash\",\n  \"192\": \"graveaccent\",\n  \"220\": \"backslash\",\n  \"222\": \"quote\",\n  \"224\": \"cmd\",\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":560,"value":"translations = fetch(\n  'https://raw.githubusercontent.com/leanprover/vscode-lean/master/src/abbreviation/abbreviations.json'\n).then(res => res.json())","pinned":false,"mode":"js","data":null,"name":null},{"id":730,"value":"hackyReplacements = {\n  // const leader = '\\\\';\n  return {\n    ['{{}}']: '⦃⦄',\n    ['[[]]']: '⟦⟧',\n    ['<>']: '⟨⟩',\n    ['([])'] : '⟮⟯',\n    ['f<>'] : '‹›',\n  };\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":3536,"value":"md`This function handles Unicode translation:`","pinned":false,"mode":"js","data":null,"name":null},{"id":3532,"value":"function unicodeHandler(cm) {\n  // Unicode completion logic\n  const {line, ch} = cm.getCursor();\n  const lineToCursor = cm.getLine(line).slice(0, ch).match(/\\\\(.*)$/);\n  // ordinary completions\n  const replacement = lineToCursor && translations[lineToCursor[1]];\n  // hacky completions put the cursor between paired Unicode brackets\n  const hackyReplacement = lineToCursor && hackyReplacements[lineToCursor[1]];\n  if (replacement || hackyReplacement) {\n    const from = {\n      line, \n      ch: ch - lineToCursor[0].length,\n    };\n    const to = { line, ch, };\n    cm.replaceRange(replacement || hackyReplacement, from, to);\n    hackyReplacement && cm.setCursor(line, ch - lineToCursor[1].length);\n    return true;\n  }\n  return false;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":3896,"value":"md`This function is called when Tab is pressed; it translates Unicode or inserts spaces:`","pinned":false,"mode":"js","data":null,"name":null},{"id":3889,"value":"function tabHandler(cm) {\n  if (!unicodeHandler(cm)) {\n    // spaces instead of tabs\n    cm.replaceSelection(Array(cm.getOption(\"indentUnit\") + 1).join(\" \"));\n  }\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":3878,"value":"md`This function is used to format the completion menu:`","pinned":false,"mode":"js","data":null,"name":null},{"id":3881,"value":"function renderCompletion(elt, data, {doc, text, type, tactic_params}) {\n  // TODO: doc unused?\n  const desc = tactic_params ? tactic_params :\n  type.replace(/\\?([^\\s)},\\]]+)/g, '<span class=\"lean-completion-highlight\">$1</span>');\n  const completion = html`<code>${text} : <span class=\"lean-completion-desc\">${desc}</span></code>`;\n  elt.appendChild(completion);\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":3542,"value":"md`This function inserts \"hole markers\" \\`{!!}\\` into the editor:`","pinned":false,"mode":"js","data":null,"name":null},{"id":3546,"value":"function insertHole(cm) {\n  const sel = cm.getSelections();\n  cm.replaceSelections(sel.map((s) => `{!${s}!}`));\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":5140,"value":"CodeMirrorCSS = md`### CSS`","pinned":false,"mode":"js","data":null,"name":null},{"id":5139,"value":"CODE_MIRROR_THEME = 'default'","pinned":false,"mode":"js","data":null,"name":null},{"id":5138,"value":"EXTERNAL_CSS = ({\n  \"codemirror-css\": `${CodeMirrorURL}lib/codemirror.css`,\n  \"codemirror-css-lint\": `${CodeMirrorURL}addon/lint/lint.css`,\n  \"codemirror-css-hint\": `${CodeMirrorURL}addon/hint/show-hint.css`\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":5137,"value":"INTERNAL_CSS = [customCodeMirrorCSS, editorWrapperCSS, infoViewCSS].reduce((acc, obj) => ({...acc, [obj.id]: obj }), {})","pinned":false,"mode":"js","data":null,"name":null},{"id":5136,"value":"// appends the needed CodeMirror stylesheets to the document\ninitCodeMirrorCSS = () => {\n  Object.entries(EXTERNAL_CSS).forEach(([id, url]) => {\n    if (!document.getElementById(id)) {\n      document.head.appendChild(html`<link id='${id}' href='${url}' rel='stylesheet'>`);\n    }\n  });\n  Object.entries(INTERNAL_CSS).forEach(([id, obj]) => {\n    if (!document.getElementById(id)) {\n      document.body.appendChild(obj);\n    }\n  });\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":5135,"value":"SPACING_BASELINE = \"4px\"","pinned":false,"mode":"js","data":null,"name":null},{"id":5134,"value":"md`The next cells contain the custom style rules we apply to the editor:`","pinned":false,"mode":"js","data":null,"name":null},{"id":5133,"value":"editorWrapperCSS = html`<style id='editor-wrapper-css'>\n\n.${CSS_CLASSES.LEAN_EDITOR_BUTTON_WRAPPER} {\n  float: right;\n}\n\n.${CSS_CLASSES.LEAN_EDITOR_BUTTON_WRAPPER}, .${CSS_CLASSES.LEAN_EDITOR_TOOLBAR} {\n  display: flex;\n  flex-direction: row;\n  flex-wrap: wrap;\n}\n\n.${CSS_CLASSES.LEAN_EDITOR_CONTAINER} {\n  clear: right;\n  background: ${config.initColor};\n  padding: ${SPACING_BASELINE};\n  display: flex;\n  flex-direction: row; /* place CodeMirror and info view in a row */\n}\n\n/* allow CodeMirror and info view container to stretch their content vertically */\n.${CSS_CLASSES.CODE_MIRROR_CONTAINER}, .${CSS_CLASSES.INFO_VIEW} {\n  display: flex;\n  flex-direction: column;\n  background: white;\n  border: solid 1px;\n}\n\n.${CSS_CLASSES.CODE_MIRROR_CONTAINER}, .${CSS_CLASSES.INFO_VIEW_CODE_BLOCK} {\n  font-size: 11.5px;\n}\n\n/* vertically stretch the last child of the editor and info view */\n.${CSS_CLASSES.CODE_MIRROR_CONTAINER} >:last-child, .${CSS_CLASSES.INFO_VIEW} >:last-child {\n  flex-grow: 1;\n}\n\n.${CSS_CLASSES.LEAN_EDITOR_CONTAINER}.${CSS_CLASSES.RUNNING} {\n  background: ${config.runningColor};\n}\n\n.${CSS_CLASSES.LEAN_EDITOR_CONTAINER}.${CSS_CLASSES.STOPPED} {\n  background: ${config.stoppedColor};\n}\n\n.${CSS_CLASSES.LEAN_EDITOR_CONTAINER}.${CSS_CLASSES.DONE} {\n  background: ${config.doneColor};\n}\n\n@media (max-width: ${config.stackWidth}) {\n  .${CSS_CLASSES.LEAN_EDITOR_CONTAINER} {\n    /* stack info view and editor vertically */\n    flex-direction: column;\n  }\n}\n\n</style>`","pinned":false,"mode":"js","data":null,"name":null},{"id":5132,"value":"customCodeMirrorCSS = html`<style id='codemirror-css-custom'>\n\n.${CSS_CLASSES.CODE_MIRROR_LINT_TOOLTIP} {\n  padding ${SPACING_BASELINE};\n  white-space: normal;\n}\n\n.${CSS_CLASSES.CODE_MIRROR_LINT_TOOLTIP}, .${CSS_CLASSES.CODE_MIRROR_HINT} {\n  background: #f7f7f7;\n}\n\n/* ----------------- */\n/* GUTTER BUSY STYLE */\n/* ----------------- */\n.${CSS_CLASSES.CODE_MIRROR_GUTTER_BUSY} {\n  border-right: solid orange 2px;\n}\n\n/* ---------------------- */\n/* CODEMIRROR HINT STYLES */\n/* ---------------------- */\n.${CSS_CLASSES.CODE_MIRROR_HINT}.${CSS_CLASSES.CODE_MIRROR_HINT_ACTIVE} {\n  color: black;\n  background: #ccf2ff;\n}\n\n.${CSS_CLASSES.CODE_MIRROR_HINT}:hover:not(.${\n  CSS_CLASSES.CODE_MIRROR_HINT_ACTIVE\n}) {\n  background: #eeeeee;\n}\n\n.${CSS_CLASSES.LEAN_COMPLETION_HIGHLIGHT} {\n  color: #0052cc;\n}\n\n.${CSS_CLASSES.LEAN_COMPLETION_DESC} {\n  color: #444444;\n}\n\n/* ---------------------- */\n/* CODEMIRROR EDITOR STYLES */\n/* ---------------------- */\n.${CSS_CLASSES.CODE_MIRROR_CONTAINER} {\n  flex: 1 0 ${config.editorPercentage * 100}%;\n  margin-right: ${SPACING_BASELINE}; /* margin to info view */\n}\n\n.${CSS_CLASSES.CODE_MIRROR_CONTAINER}:only-child {\n  margin-right: 0; /* if cm is the only child, there is no info view --> no right margin needed */\n}\n\n.${CSS_CLASSES.CODE_MIRROR} {\n  height: auto;\n}\n\n/* Change colours in read-only mode */\n.${CSS_CLASSES.CODE_MIRROR}.${CSS_CLASSES.READONLY} {\n  background: #f7f7f7;\n}\n\n.${CSS_CLASSES.CODE_MIRROR_GUTTERS}.${CSS_CLASSES.READONLY} {\n  background: white;\n}\n\n@media (max-width: ${config.stackWidth}) {\n  .${CSS_CLASSES.CODE_MIRROR_CONTAINER} {\n    /* no space to the right needed anymore as info view is below the editor */\n    margin-right: 0;\n  }\n}\n\n</style>`","pinned":false,"mode":"js","data":null,"name":null},{"id":5131,"value":"infoViewCSS = html`<style id='info-view-css'>\n\n.${CSS_CLASSES.INFO_VIEW} {\n  padding: ${SPACING_BASELINE};\n  flex-basis: ${(1-config.editorPercentage)*100}%;\n  overflow: auto;\n  min-height: 20px; /* info view will stretch automatically, but give it at least some height to see the icons */\n  font-size: 13px;\n  line-height: 1.3;\n}\n\n.${CSS_CLASSES.INFO_VIEW_BUTTONS} {\n  margin-left: auto; /* float to the right */\n  height: ${SPACING_BASELINE}; /* Needed to make float with wrap work */\n}\n\n.${CSS_CLASSES.INFO_VIEW_BUTTONS} .${CSS_CLASSES.ACTIVE} {\n  opacity: 1;\n}\n\n.${CSS_CLASSES.INFO_VIEW_BUTTONS} .${CSS_CLASSES.INACTIVE} {\n  opacity: 0.25;\n}\n\n.${CSS_CLASSES.INFO_VIEW_INFO_HEADER} {\n  border-bottom: solid 1px;\n  font-weight: bold;\n  font-family: sans-serif;\n  margin-bottom: ${SPACING_BASELINE};\n  margin-top: ${SPACING_BASELINE};\n}\n\n.${CSS_CLASSES.INFO_VIEW_CODE_BLOCK} {\n  white-space: pre-wrap;\n  overflow-wrap: break-word;\n}\n\n.${CSS_CLASSES.INFO_VIEW_CODE_BLOCK}, .${CSS_CLASSES.INFO_VIEW_TOGGLE_DOC} code {\n  font-family: monospace;\n}\n\n.${CSS_CLASSES.INFO_VIEW_TOGGLE_DOC} li::before {\n  position: static; /* Override an observablehq.com style for bullet points in md function */\n}\n\n.${CSS_CLASSES.HOLE_MSG_CONTAINER} .${CSS_CLASSES.INFO_VIEW_INFO_HEADER} {\n  color: blue\n}\n\n.${CSS_CLASSES.INFO_VIEW_TOGGLE_DOC} {\n  cursor: pointer;\n}\n\n</style>`","pinned":false,"mode":"js","data":null,"name":null},{"id":5130,"value":"md`⚠️ The functions in this notebook that depend on these style rules automatically append them to the document if they are not already present. Thus users won't need to explicitly import them into their notebooks (unless they want to override some part of it).`","pinned":false,"mode":"js","data":null,"name":null},{"id":5129,"value":"md`Here we define the list of class names that we use`","pinned":false,"mode":"js","data":null,"name":null},{"id":5128,"value":"CSS_CLASSES = ({\n  READONLY: \"readonly\",\n  ACTIVE: \"active\",\n  INACTIVE: \"inactive\",\n  RUNNING: \"running\",\n  DONE: \"done\",\n  STOPPED: \"stopped\",\n  CODE_MIRROR: \"CodeMirror\",\n  CODE_MIRROR_GUTTERS: \"CodeMirror-gutters\",\n  CODE_MIRROR_GUTTER_BUSY: \"CodeMirror-gutter-busy\",\n  CODE_MIRROR_HINT: \"CodeMirror-hint\",\n  CODE_MIRROR_HINT_ACTIVE: \"CodeMirror-hint-active\",\n  CODE_MIRROR_LINT_TOOLTIP: \"CodeMirror-lint-tooltip\",\n  LEAN_COMPLETION_DESC: \"lean-completion-desc\",\n  LEAN_COMPLETION_HIGHLIGHT: \"lean-completion-highlight\",\n  LEAN_EDITOR_WRAPPER: \"lean-editor-wrapper\",\n  LEAN_EDITOR_CONTAINER: \"lean-editor-container\",\n  LEAN_EDITOR_HEADER: \"lean-editor-header\",\n  LEAN_EDITOR_SAVE_BUTTON: \"lean-editor-save-button\",\n  LEAN_EDITOR_UNDO_BUTTON: \"lean-editor-undo-button\",\n  LEAN_EDITOR_REDO_BUTTON: \"lean-editor-redo-button\",\n  LEAN_EDITOR_HOLE_BUTTON: \"lean-editor-hole-button\",\n  LEAN_EDITOR_UNICODE_BUTTON: \"lean-editor-unicode-button\",\n  LEAN_EDITOR_COMPLETE_BUTTON: \"lean-editor-complete-button\",\n  LEAN_EDITOR_RESET_BUTTON: \"lean-editor-reset-button\",\n  LEAN_EDITOR_COMMENT_BUTTON: \"lean-editor-comment-button\",\n  LEAN_EDITOR_TOOLBAR: \"lean-editor-toolbar\",\n  LEAN_EDITOR_TOGGLE_TOOLBAR_BUTTON: \"lean-editor-toggle-toolbar-button\",\n  LEAN_EDITOR_BUTTON_WRAPPER: \"lean-editor-button-wrapper\",\n  CODE_MIRROR_CONTAINER: \"cm-container\",\n  INFO_VIEW: \"info-view\",\n  INFO_VIEW_GOAL_ICON: \"info-view-goal-icon\",\n  INFO_VIEW_MSGS_ICON: \"info-view-msgs-icon\",\n  INFO_VIEW_BUTTONS: \"info-view-buttons\",\n  INFO_VIEW_GOAL_WIDGET: \"info-view-goal-widget\",\n  INFO_VIEW_MSGS_WIDGET: \"info-view-msgs-widget\",\n  HOLE_MSG_CONTAINER: \"hole-msg-container\",\n  INFO_VIEW_INFO_HEADER: \"info-view-info-header\",\n  INFO_VIEW_CODE_BLOCK: \"info-view-code-block\",\n  INFO_VIEW_TOGGLE_DOC: \"info-view-toggle-doc\"\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":615,"value":"TODO = md`## 4. TODO:\n\n- Refactor \\`leanEditor\\` into smaller components!\n- Start moving code to separate repos:\n  - CodeMirror Lean mode / linting\n- Consider using \\`Generators.disposable\\` instead of passing \\`invalidation\\` promises around?\n  - \\`InfoView\\` / \\`LeanFile\\` are classes and should have some indication of when to dispose their message handlers\n  - If \\`leanEditor\\` returns a generator, then using it within a cell or other code becomes a lot harder\n- Think harder about caching. It seems that \"hard-refreshing\" Firefox doesn't always replace the cached version of the WASM files (maybe because it's downloaded via a \\`fetch\\` call?).\n  - Use BroadcastChannel to coordinate locking IndexedDB? (Not supported in Safari...)\n- \\`vscode-lean\\` features:\n  - Truncate messages longer than X / show only first X messages\n  - Syntax highlighting for tactic state / messages\n  - \"goals accomplished\" instead of \"no goals\" (like in \\`vscode-lean\\`)?\n  - Links or hover in message headers\n  - Scroll infoview when cursor moves in editor\n- Fix up the CSS in general:\n  - Editor seems to expand to height of InfoView regardless of \\`height\\` option?\n  - Draggable boundary between editor and infoview / adjustable height\n  - Add styles / icons for \\`'information'\\` severity (see [this CSS file for CodeMirror's linting](https://github.com/codemirror/CodeMirror/blob/master/addon/lint/lint.css))\n  - Restyle editor?\n- Consider using CodeMirror's [\\`linkedDoc\\` feature](https://codemirror.net/doc/manual.html#linkedDoc)\n- Consider using CodeMirror's \\`firstLineNumber\\` option to replace \\`min_line()\\` stuff\n- Do something about the shared code between \\`LeanFile\\` and \\`leanEditor\\`. (Should \\`leanEditor\\` be a class?)\n- Do more with the \"source\" properties of server responses (containing links to github)\n- Is there any good interface for the Server's \\`search\\` command?\n- Add cache-clearing buttons to toolbar and a message about them if \"\\`file '/library/init/default.lean' not found\\`\"\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":1912,"value":"md`### Version history:\n\n- [2019/07/28](https://observablehq.com/@bryangingechen/hello-lean-prover@4946): first release\n- [2019/07/31](https://observablehq.com/@bryangingechen/hello-lean-prover@5055): support [locally-running Lean via websocketd](https://observablehq.com/@bryangingechen/lean-websocketd), editor background turns red when server is dead, clean up object properties\n- [2019/08/01](https://observablehq.com/@bryangingechen/hello-lean-prover@5065): improve and clarify text (thanks [@kappelmann](https://observablehq.com/@kappelmann)!)\n- [2019/08/08](https://observablehq.com/@bryangingechen/hello-lean-prover@5081): Merged suggestion from @kappelmann: \"Fix CSS (right outline was invisible) of cm-container-full for small screens by using \\`flex-grow: 1;\\` instead of \\`width: 100%\\`\" and also remove workaround for [this Firefox issue](https://talk.observablehq.com/t/misalignment-of-source-cells-in-firefox-when-divs-have-non-integer-px-heights/2029).\n- [2019/08/12](https://observablehq.com/@bryangingechen/hello-lean-prover@5118): Fixed cursor positioning after Unicode translation for paired brackets and added French quotes \"\\`‹›\\`\" to [\\`hackyReplacements\\`](#hackyReplacements).\n- [2019/08/15](https://observablehq.com/@bryangingechen/hello-lean-prover@5203): Many, many improvements from a suggestion by @kappelmann: \"replaced hard-coded CSS classes, fixed some styling (e.g. resizing with CSS), [...], add highlighting in goal state, etc.\" Breaking changes:\n  - \\`refresh()\\` method removed from \\`value\\` object of \\`leanEditor()\\`\n  - \\`height\\` option to \\`leanEditor()\\` must now be a CSS length string, not a number.\n  - many CSS class names changed. Refer to [\\`CSS_CLASSES\\`](#CSS_CLASSES).\n- [2019/08/18](https://observablehq.com/@bryangingechen/hello-lean-prover@5205): Merged suggestion from @kappelmann: “fix right margin CSS if there is no info view”.\n- [2019/08/20](https://observablehq.com/@bryangingechen/hello-lean-prover@5217): expose \\`updateInfoview()\\` as a method of the \\`leanEditor\\`'s' \\`value\\` object.\n- [2019/08/21](https://observablehq.com/@bryangingechen/hello-lean-prover@5236): Breaking change: rename \\`updateInfoview()\\` to \\`updateInfoView()\\`, and merged suggestion from @kappelmann: “Add debouncing for info view update and change download URL”.\n- [2019/08/27](https://observablehq.com/@bryangingechen/hello-lean-prover@5238): Merged suggestion from @kappelmann: “When passing a function to header, it should be displayed in a \\`<span />\\` (just like the default header does).”\n- [2019/09/13](https://observablehq.com/@bryangingechen/hello-lean-prover@5245): Add workaround from [this issue](https://github.com/codemirror/CodeMirror/issues/4895) so that scrollbars appear in Safari (thanks @kappelmann!). **Warning: This workaround broke the editor on narrow screens. (Fixed in 2019/09/16 update.) **\n- [2019/09/16](https://observablehq.com/@bryangingechen/hello-lean-prover@5272): Simplified height handling; editor should now scroll properly in Firefox, Chrome and Safari on both wide and narrow screens.\n- [2019/12/15](https://observablehq.com/@bryangingechen/hello-lean-prover@5307): Several additions:\n  - changed CDN to https://gitcdn.link/\n  - added message about WASM being broken in Safari\n  - added doc and module comment handling to syntax highlighting \n- [2020/03/06](https://observablehq.com/@bryangingechen/hello-lean-prover@5313): change host and update default file for new version of Lean\n- [2020/05/19](https://observablehq.com/@bryangingechen/hello-lean-prover@5319): update default file for new version of Lean\n- [2020/05/21](https://observablehq.com/@bryangingechen/hello-lean-prover@5327): Switch to \\`@bryangingechen/lean-client-js-browser\\` v1.4.0.\n- [2020/11/03](https://observablehq.com/@bryangingechen/hello-lean-prover@5346): Switch to \\`lean-client-js-browser\\` v2.0.1 and update linting code for [changes in CodeMirror 5.58.0](https://github.com/codemirror/CodeMirror/commit/18aa69e17cc7703f106fbe03992456b8e59e8cdc).\n- 2021/01/26: update link to \\`translations.json\\` (thanks @kappelmann!).\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":1942,"value":"LICENSE = md`## 5. LICENSE:\n\nThis notebook incorporates code from the following open source projects:\n- [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0.html):\n  - [\\`lean-client-js\\`](https://github.com/leanprover/lean-client-js)\n  - [\\`lean-web-editor\\`](https://github.com/leanprover/lean-web-editor)\n  - [\\`vscode-lean\\`](https://github.com/leanprover/vscode-lean)\n- [MIT License](https://opensource.org/licenses/MIT):\n  - [CodeMirror](https://github.com/codemirror/codemirror): Copyright (C) 2017 by Marijn Haverbeke and others,\n  - [\\`CodeMirror-Extension\\`](https://github.com/angelozerr/CodeMirror-Extension): Copyright (c) 2015 Angelo\n\nThis notebook is licensed under the Apache License 2.0.\n`","pinned":false,"mode":"js","data":null,"name":null}],"resolutions":[],"schedule":null,"last_view_time":null}