mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Upgrade to CodeMirror 6, add SQL autocomplete (#1893)
* Upgrade to CodeMirror 6 * Update contributing docs * Change how resizing works * Define a custom SQLite autocomplete dialect * Add meta-enter to submit * Add fixture schema for testing
This commit is contained in:
parent
6f610e1d94
commit
ae11fa5887
11 changed files with 1017 additions and 76 deletions
1
datasette/static/cm-editor-6.0.1.bundle.js
Normal file
1
datasette/static/cm-editor-6.0.1.bundle.js
Normal file
File diff suppressed because one or more lines are too long
74
datasette/static/cm-editor-6.0.1.js
Normal file
74
datasette/static/cm-editor-6.0.1.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { EditorView, basicSetup } from "codemirror";
|
||||
import { keymap } from "@codemirror/view";
|
||||
import { sql, SQLDialect } from "@codemirror/lang-sql";
|
||||
|
||||
// A variation of SQLite from lang-sql https://github.com/codemirror/lang-sql/blob/ebf115fffdbe07f91465ccbd82868c587f8182bc/src/sql.ts#L231
|
||||
const SQLite = SQLDialect.define({
|
||||
// Based on https://www.sqlite.org/lang_keywords.html based on likely keywords to be used in select queries
|
||||
// https://github.com/simonw/datasette/pull/1893#issuecomment-1316401895:
|
||||
keywords:
|
||||
"and as asc between by case cast count current_date current_time current_timestamp desc distinct each else escape except exists explain filter first for from full generated group having if in index inner intersect into isnull join last left like limit not null or order outer over pragma primary query raise range regexp right rollback row select set table then to union unique using values view virtual when where",
|
||||
// https://www.sqlite.org/datatype3.html
|
||||
types: "null integer real text blob",
|
||||
builtin: "",
|
||||
operatorChars: "*+-%<>!=&|/~",
|
||||
identifierQuotes: '`"',
|
||||
specialVar: "@:?$",
|
||||
});
|
||||
|
||||
// Utility function from https://codemirror.net/docs/migration/
|
||||
export function editorFromTextArea(textarea, conf = {}) {
|
||||
// This could also be configured with a set of tables and columns for better autocomplete:
|
||||
// https://github.com/codemirror/lang-sql#user-content-sqlconfig.tables
|
||||
let view = new EditorView({
|
||||
doc: textarea.value,
|
||||
extensions: [
|
||||
keymap.of([
|
||||
{
|
||||
key: "Shift-Enter",
|
||||
run: function () {
|
||||
textarea.value = view.state.doc.toString();
|
||||
textarea.form.submit();
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "Meta-Enter",
|
||||
run: function () {
|
||||
textarea.value = view.state.doc.toString();
|
||||
textarea.form.submit();
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]),
|
||||
// This has to be after the keymap or else the basicSetup keys will prevent
|
||||
// Meta-Enter from running
|
||||
basicSetup,
|
||||
EditorView.lineWrapping,
|
||||
sql({
|
||||
dialect: SQLite,
|
||||
schema: conf.schema,
|
||||
tables: conf.tables,
|
||||
defaultTableName: conf.defaultTableName,
|
||||
defaultSchemaName: conf.defaultSchemaName,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Idea taken from https://discuss.codemirror.net/t/resizing-codemirror-6/3265.
|
||||
// Using CSS resize: both and scheduling a measurement when the element changes.
|
||||
let editorDOM = view.contentDOM.closest(".cm-editor");
|
||||
let observer = new ResizeObserver(function () {
|
||||
view.requestMeasure();
|
||||
});
|
||||
observer.observe(editorDOM, { attributes: true });
|
||||
|
||||
textarea.parentNode.insertBefore(view.dom, textarea);
|
||||
textarea.style.display = "none";
|
||||
if (textarea.form) {
|
||||
textarea.form.addEventListener("submit", () => {
|
||||
textarea.value = view.state.doc.toString();
|
||||
});
|
||||
}
|
||||
return view;
|
||||
}
|
||||
8
datasette/static/cm-resize-1.0.1.min.js
vendored
8
datasette/static/cm-resize-1.0.1.min.js
vendored
|
|
@ -1,8 +0,0 @@
|
|||
/*!
|
||||
* cm-resize v1.0.1
|
||||
* https://github.com/Sphinxxxx/cm-resize
|
||||
*
|
||||
* Copyright 2017-2018 Andreas Borgen (https://github.com/Sphinxxxx)
|
||||
* Released under the MIT license.
|
||||
*/
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.cmResize=t()}(this,function(){"use strict";return document.documentElement.firstElementChild.appendChild(document.createElement("style")).textContent=".cm-resize-handle{display:block;position:absolute;bottom:0;right:0;z-index:99;width:18px;height:18px;background:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0,0 16,16'%3E%3Cpath stroke='gray' stroke-width='2' d='M-1,12 l18,-18 M-1,18 l18,-18 M-1,24 l18,-18 M-1,30 l18,-18'/%3E%3C/svg%3E\") center/cover;box-shadow:inset -1px -1px 0 0 silver;cursor:nwse-resize}",function(r,e){var t,c=(e=e||{}).minWidth||200,l=e.minHeight||100,s=!1!==e.resizableWidth,d=!1!==e.resizableHeight,n=e.cssClass||"cm-resize-handle",o=r.display.wrapper,i=e.handle||((t=o.appendChild(document.createElement("div"))).className=n,t),a=o.querySelector(".CodeMirror-vscrollbar"),u=o.querySelector(".CodeMirror-hscrollbar");function h(){e.handle||(a.style.bottom="18px",u.style.right="18px")}r.on("update",h),h();var f=void 0,m=void 0;return function(e){var t=Element.prototype;t.matches||(t.matches=t.msMatchesSelector||t.webkitMatchesSelector),t.closest||(t.closest=function(e){var t=this;do{if(t.matches(e))return t;t="svg"===t.tagName?t.parentNode:t.parentElement}while(t);return null});var l=(e=e||{}).container||document.documentElement,n=e.selector,o=e.callback||console.log,i=e.callbackDragStart,a=e.callbackDragEnd,r=e.callbackClick,c=e.propagateEvents,s=!1!==e.roundCoords,d=!1!==e.dragOutside,u=e.handleOffset||!1!==e.handleOffset,h=null;switch(u){case"center":h=!0;break;case"topleft":case"top-left":h=!1}var f=void 0,m=void 0,p=void 0;function v(e,t,n,o){var i=e.clientX,a=e.clientY;function r(e,t,n){return Math.max(t,Math.min(e,n))}if(t){var c=t.getBoundingClientRect();i-=c.left,a-=c.top,n&&(i-=n[0],a-=n[1]),o&&(i=r(i,0,c.width),a=r(a,0,c.height)),t!==l&&(null!==h?h:"circle"===t.nodeName||"ellipse"===t.nodeName)&&(i-=c.width/2,a-=c.height/2)}return s?[Math.round(i),Math.round(a)]:[i,a]}function g(e){e.preventDefault(),c||e.stopPropagation()}function w(e){(f=n?n instanceof Element?n.contains(e.target)?n:null:e.target.closest(n):{})&&(g(e),m=n&&u?v(e,f):[0,0],p=v(e,l,m),s&&(p=p.map(Math.round)),i&&i(f,p))}function b(e){if(f){g(e);var t=v(e,l,m,!d);o(f,t,p)}}function E(e){if(f){if(a||r){var t=v(e,l,m,!d);r&&p[0]===t[0]&&p[1]===t[1]&&r(f,p),a&&a(f,t,p)}f=null}}function x(e){E(C(e))}function M(e){return void 0!==e.buttons?1===e.buttons:1===e.which}function k(e,t){1===e.touches.length?t(C(e)):E(e)}function C(e){var t=e.targetTouches[0];return t||(t=e.changedTouches[0]),t.preventDefault=e.preventDefault.bind(e),t.stopPropagation=e.stopPropagation.bind(e),t}l.addEventListener("mousedown",function(e){M(e)&&w(e)}),l.addEventListener("touchstart",function(e){k(e,w)}),window.addEventListener("mousemove",function(e){f&&(M(e)?b(e):E(e))}),window.addEventListener("touchmove",function(e){k(e,b)}),window.addEventListener("mouseup",function(e){f&&!M(e)&&E(e)}),l.addEventListener("touchend",x),l.addEventListener("touchcancel",x)}({container:i.offsetParent,selector:i,callbackDragStart:function(e,t){f=t,m=[o.clientWidth,o.clientHeight]},callback:function(e,t){var n=t[0]-f[0],o=t[1]-f[1],i=s?Math.max(c,m[0]+n):null,a=d?Math.max(l,m[1]+o):null;r.setSize(i,a)}}),i}});
|
||||
File diff suppressed because one or more lines are too long
1
datasette/static/codemirror-5.57.0.min.css
vendored
1
datasette/static/codemirror-5.57.0.min.css
vendored
File diff suppressed because one or more lines are too long
11
datasette/static/codemirror-5.57.0.min.js
vendored
11
datasette/static/codemirror-5.57.0.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1,14 +1,17 @@
|
|||
<script src="{{ base_url }}-/static/sql-formatter-2.3.3.min.js" defer></script>
|
||||
<script src="{{ base_url }}-/static/codemirror-5.57.0.min.js"></script>
|
||||
<link rel="stylesheet" href="{{ base_url }}-/static/codemirror-5.57.0.min.css" />
|
||||
<script src="{{ base_url }}-/static/codemirror-5.57.0-sql.min.js"></script>
|
||||
<script src="{{ base_url }}-/static/cm-resize-1.0.1.min.js"></script>
|
||||
<script src="{{ base_url }}-/static/cm-editor-6.0.1.bundle.js"></script>
|
||||
<style>
|
||||
.CodeMirror { height: auto; min-height: 70px; width: 80%; border: 1px solid #ddd; }
|
||||
.cm-resize-handle {
|
||||
background: url("data:image/svg+xml,%3Csvg%20aria-labelledby%3D%22cm-drag-to-resize%22%20role%3D%22img%22%20fill%3D%22%23ccc%22%20stroke%3D%22%23ccc%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2016%2016%22%20width%3D%2216%22%20height%3D%2216%22%3E%0A%20%20%3Ctitle%20id%3D%22cm-drag-to-resize%22%3EDrag%20to%20resize%3C%2Ftitle%3E%0A%20%20%3Cpath%20fill-rule%3D%22evenodd%22%20d%3D%22M1%202.75A.75.75%200%20011.75%202h12.5a.75.75%200%20110%201.5H1.75A.75.75%200%20011%202.75zm0%205A.75.75%200%20011.75%207h12.5a.75.75%200%20110%201.5H1.75A.75.75%200%20011%207.75zM1.75%2012a.75.75%200%20100%201.5h12.5a.75.75%200%20100-1.5H1.75z%22%3E%3C%2Fpath%3E%0A%3C%2Fsvg%3E");
|
||||
background-repeat: no-repeat;
|
||||
box-shadow: none;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
.cm-editor {
|
||||
resize: both;
|
||||
overflow: hidden;
|
||||
min-height: 70px;
|
||||
width: 80%;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
/* Fix autocomplete icon positioning. The icon element gets border-box sizing set due to
|
||||
the global reset, but this causes overlapping icon and text. Markup:
|
||||
`<div class="cm-completionIcon cm-completionIcon-keyword" aria-hidden="true"></div>` */
|
||||
.cm-completionIcon {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,38 +1,117 @@
|
|||
<script>
|
||||
window.onload = () => {
|
||||
const schema = {
|
||||
"123_starts_with_digits": ["content"],
|
||||
"Table With Space In Name": ["content", "pk"],
|
||||
attraction_characteristic: ["name", "pk"],
|
||||
binary_data: ["data"],
|
||||
complex_foreign_keys: ["f1", "f2", "f3", "pk"],
|
||||
compound_primary_key: ["content", "pk1", "pk2"],
|
||||
compound_three_primary_keys: ["content", "pk1", "pk2", "pk3"],
|
||||
custom_foreign_key_label: ["foreign_key_with_custom_label", "pk"],
|
||||
facet_cities: ["id", "name"],
|
||||
facetable: [
|
||||
"_city_id",
|
||||
"_neighborhood",
|
||||
"complex_array",
|
||||
"created",
|
||||
"distinct_some_null",
|
||||
"n",
|
||||
"on_earth",
|
||||
"pk",
|
||||
"planet_int",
|
||||
"state",
|
||||
"tags",
|
||||
],
|
||||
foreign_key_references: [
|
||||
"foreign_key_compound_pk1",
|
||||
"foreign_key_compound_pk2",
|
||||
"foreign_key_with_blank_label",
|
||||
"foreign_key_with_label",
|
||||
"foreign_key_with_no_label",
|
||||
"pk",
|
||||
],
|
||||
infinity: ["value"],
|
||||
no_primary_key: ["a", "b", "c", "content"],
|
||||
primary_key_multiple_columns: ["content", "content2", "id"],
|
||||
primary_key_multiple_columns_explicit_label: ["content", "content2", "id"],
|
||||
roadside_attraction_characteristics: ["attraction_id", "characteristic_id"],
|
||||
roadside_attractions: [
|
||||
"address",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"name",
|
||||
"pk",
|
||||
"url",
|
||||
],
|
||||
searchable: ["name with . and spaces", "pk", "text1", "text2"],
|
||||
searchable_fts: [
|
||||
"__langid",
|
||||
"docid",
|
||||
"name with . and spaces",
|
||||
"searchable_fts",
|
||||
"text1",
|
||||
"text2",
|
||||
],
|
||||
searchable_fts_docsize: ["docid", "size"],
|
||||
searchable_fts_segdir: [
|
||||
"end_block",
|
||||
"idx",
|
||||
"leaves_end_block",
|
||||
"level",
|
||||
"root",
|
||||
"start_block",
|
||||
],
|
||||
searchable_fts_segments: ["block", "blockid"],
|
||||
searchable_fts_stat: ["id", "value"],
|
||||
searchable_tags: ["searchable_id", "tag"],
|
||||
select: ["and", "group", "having", "json"],
|
||||
simple_primary_key: ["content", "id"],
|
||||
sortable: [
|
||||
"content",
|
||||
"pk1",
|
||||
"pk2",
|
||||
"sortable",
|
||||
"sortable_with_nulls",
|
||||
"sortable_with_nulls_2",
|
||||
"text",
|
||||
],
|
||||
"table/with/slashes.csv": ["content", "pk"],
|
||||
tags: ["tag"],
|
||||
units: ["distance", "frequency", "pk"],
|
||||
};
|
||||
|
||||
window.onload = () => {
|
||||
const sqlFormat = document.querySelector("button#sql-format");
|
||||
const readOnly = document.querySelector("pre#sql-query");
|
||||
const sqlInput = document.querySelector("textarea#sql-editor");
|
||||
if (sqlFormat && !readOnly) {
|
||||
sqlFormat.hidden = false;
|
||||
sqlFormat.hidden = false;
|
||||
}
|
||||
if (sqlInput) {
|
||||
var editor = CodeMirror.fromTextArea(sqlInput, {
|
||||
lineNumbers: true,
|
||||
mode: "text/x-sql",
|
||||
lineWrapping: true,
|
||||
var editor = (window.editor = cm.editorFromTextArea(sqlInput, {
|
||||
schema,
|
||||
}));
|
||||
if (sqlFormat) {
|
||||
sqlFormat.addEventListener("click", (ev) => {
|
||||
const formatted = sqlFormatter.format(editor.state.doc.toString());
|
||||
editor.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: editor.state.doc.length,
|
||||
insert: formatted,
|
||||
},
|
||||
});
|
||||
});
|
||||
editor.setOption("extraKeys", {
|
||||
"Shift-Enter": function() {
|
||||
document.getElementsByClassName("sql")[0].submit();
|
||||
},
|
||||
Tab: false
|
||||
});
|
||||
if (sqlFormat) {
|
||||
sqlFormat.addEventListener("click", ev => {
|
||||
editor.setValue(sqlFormatter.format(editor.getValue()));
|
||||
})
|
||||
}
|
||||
cmResize(editor, {resizableWidth: false});
|
||||
}
|
||||
}
|
||||
if (sqlFormat && readOnly) {
|
||||
const formatted = sqlFormatter.format(readOnly.innerHTML);
|
||||
if (formatted != readOnly.innerHTML) {
|
||||
sqlFormat.hidden = false;
|
||||
sqlFormat.addEventListener("click", ev => {
|
||||
readOnly.innerHTML = formatted;
|
||||
})
|
||||
}
|
||||
const formatted = sqlFormatter.format(readOnly.innerHTML);
|
||||
if (formatted != readOnly.innerHTML) {
|
||||
sqlFormat.hidden = false;
|
||||
sqlFormat.addEventListener("click", (ev) => {
|
||||
readOnly.innerHTML = formatted;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue