From 2d0165eccf5d74e09f259cd5bd5dadf7a20aa13a Mon Sep 17 00:00:00 2001 From: Ilya Grigoriev Date: Sun, 10 Mar 2024 16:56:42 -0700 Subject: [PATCH] Move MergeView to a separate file --- .../{index-De52_UIv.js => index-hsJpV_Q8.js} | 6 +- webapp/dist/index.html | 2 +- webapp/src/main.ts | 180 +---------------- webapp/src/merge_state.ts | 183 ++++++++++++++++++ 4 files changed, 188 insertions(+), 183 deletions(-) rename webapp/dist/assets/{index-De52_UIv.js => index-hsJpV_Q8.js} (98%) create mode 100644 webapp/src/merge_state.ts diff --git a/webapp/dist/assets/index-De52_UIv.js b/webapp/dist/assets/index-hsJpV_Q8.js similarity index 98% rename from webapp/dist/assets/index-De52_UIv.js rename to webapp/dist/assets/index-hsJpV_Q8.js index 10ac1b0..1436784 100644 --- a/webapp/dist/assets/index-De52_UIv.js +++ b/webapp/dist/assets/index-hsJpV_Q8.js @@ -32,7 +32,7 @@ b`.split(/\n/).length!=3?function(e){for(var t=0,i=[],r=e.length;t<=r;){var n=e. * SPDX-License-Identifier: BSD-3-Clause */const Si=globalThis,cn=Si.trustedTypes,Po=cn?cn.createPolicy("lit-html",{createHTML:O=>O}):void 0,qo="$lit$",qt=`lit$${(Math.random()+"").slice(9)}$`,Zo="?"+qt,wf=`<${Zo}>`,gr=document,Ti=()=>gr.createComment(""),ki=O=>O===null||typeof O!="object"&&typeof O!="function",Qo=Array.isArray,Cf=O=>Qo(O)||typeof(O==null?void 0:O[Symbol.iterator])=="function",Sl=`[ \f\r]`,wi=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,Bo=/-->/g,Ro=/>/g,hr=RegExp(`>|${Sl}(?:([^\\s"'>=/]+)(${Sl}*=${Sl}*(?:[^ -\f\r"'\`<>=]|("|')|))|$)`,"g"),zo=/'/g,Uo=/"/g,Jo=/^(?:script|style|textarea|title)$/i,Sf=O=>(b,...T)=>({_$litType$:O,strings:b,values:T}),cr=Sf(1),xi=Symbol.for("lit-noChange"),Ye=Symbol.for("lit-nothing"),Go=new WeakMap,dr=gr.createTreeWalker(gr,129);function jo(O,b){if(!Array.isArray(O)||!O.hasOwnProperty("raw"))throw Error("invalid template strings array");return Po!==void 0?Po.createHTML(b):b}const Lf=(O,b)=>{const T=O.length-1,N=[];let M,K=b===2?"":"",h=wi;for(let c=0;c"?(h=M??wi,w=-1):m[1]===void 0?w=-2:(w=h.lastIndex-m[2].length,v=m[1],h=m[3]===void 0?hr:m[3]==='"'?Uo:zo):h===Uo||h===zo?h=hr:h===Bo||h===Ro?h=wi:(h=hr,M=void 0);const _=h===hr&&O[c+1].startsWith("/>")?" ":"";K+=h===wi?d+wf:w>=0?(N.push(v),d.slice(0,w)+qo+d.slice(w)+qt+_):d+qt+(w===-2?c:_)}return[jo(O,K+(O[T]||"")+(b===2?"":"")),N]};class Ai{constructor({strings:b,_$litType$:T},N){let M;this.parts=[];let K=0,h=0;const c=b.length-1,d=this.parts,[v,m]=Lf(b,T);if(this.el=Ai.createElement(v,N),dr.currentNode=this.el.content,T===2){const w=this.el.content.firstChild;w.replaceWith(...w.childNodes)}for(;(M=dr.nextNode())!==null&&d.length0){M.textContent=cn?cn.emptyScript:"";for(let _=0;_2||N[0]!==""||N[1]!==""?(this._$AH=Array(N.length-1).fill(new String),this.strings=N):this._$AH=Ye}_$AI(b,T=this,N,M){const K=this.strings;let h=!1;if(K===void 0)b=Rr(this,b,T,0),h=!ki(b)||b!==this._$AH&&b!==xi,h&&(this._$AH=b);else{const c=b;let d,v;for(b=K[0],d=0;d{const N=(T==null?void 0:T.renderBefore)??b;let M=N._$litPart$;if(M===void 0){const K=(T==null?void 0:T.renderBefore)??null;N._$litPart$=M=new _i(b.insertBefore(Ti(),K),K,void 0,T??{})}return M._$AI(O),M};function _f(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function xl(O,b=!1){const T=_f(),N=`_${T}`;return Object.defineProperty(window,N,{value:M=>(b&&Reflect.deleteProperty(window,N),O==null?void 0:O(M)),writable:!1,configurable:!0}),T}async function Vo(O,b={}){return new Promise((T,N)=>{const M=xl(h=>{T(h),Reflect.deleteProperty(window,`_${K}`)},!0),K=xl(h=>{N(h),Reflect.deleteProperty(window,`_${M}`)},!0);window.__TAURI_IPC__({cmd:O,callback:M,error:K,...b})})}async function Al(O){return Vo("tauri",O)}async function Df(O=0){return Al({__tauriModule:"Process",message:{cmd:"exit",exitCode:O}})}function Tl(O){return O.type=="Text"?O.value:null}const Ml="__TAURI__"in globalThis;async function es(O,b,T){if(Ml){let N={};return T!=null&&(N={result:T}),await Vo(O,N)}else return await ts(O,b,T)}async function ts(O,b,T){let N=null,M={};T!=null&&(N=JSON.stringify(T),M["Content-Type"]="application/json");const K=await fetch(`/api/${O}`,{method:b,body:N,headers:M});if(K.ok)return await K.json();{let h="";throw K.status<500&&(h=`Likely bug in the webapp: got response "${K.status} ${K.statusText}" for "${O}" request. Additional details, if any, follow. `),h+await K.text()}}async function _l(O){Ml?await Df(O):await ts("exit","POST",O)}async function Nf(){await _l(0)}async function Ko(){await _l(1)}async function Of(){await _l(2)}async function $o(O){return await es("save","PUT",O)}async function Ef(){const O=await es("get_merge_data","GET");for(const b in O)O[b]={left:O[b][0],right:O[b][1],edit:O[b][2]};return O}async function Wf(O,b){return Al({__tauriModule:"Event",message:{cmd:"unlisten",event:O,eventId:b}})}async function Ff(O,b,T){return Al({__tauriModule:"Event",message:{cmd:"listen",event:O,windowLabel:b,handler:xl(T)}}).then(N=>async()=>Wf(O,N))}var Xo;(function(O){O.WINDOW_RESIZED="tauri://resize",O.WINDOW_MOVED="tauri://move",O.WINDOW_CLOSE_REQUESTED="tauri://close-requested",O.WINDOW_CREATED="tauri://window-created",O.WINDOW_DESTROYED="tauri://destroyed",O.WINDOW_FOCUS="tauri://focus",O.WINDOW_BLUR="tauri://blur",O.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",O.WINDOW_THEME_CHANGED="tauri://theme-changed",O.WINDOW_FILE_DROP="tauri://file-drop",O.WINDOW_FILE_DROP_HOVER="tauri://file-drop-hover",O.WINDOW_FILE_DROP_CANCELLED="tauri://file-drop-cancelled",O.MENU="tauri://menu",O.CHECK_UPDATE="tauri://update",O.UPDATE_AVAILABLE="tauri://update-available",O.INSTALL_UPDATE="tauri://update-install",O.STATUS_UPDATE="tauri://update-status",O.DOWNLOAD_PROGRESS="tauri://update-download-progress"})(Xo||(Xo={}));async function fn(O,b){return Ff(O,null,b)}class If{constructor(b){Eo(this,"merge_views");this.merge_views=b}values(){const b={};for(const T in this.merge_views)b[T]=this.merge_views[T].editor().getValue();return b}}function Hf(O,b){let T=[],N=c=>`${c}_${O}`,M=c=>{let d=Array.from([{file:c.left,side:"left"},{file:c.right,side:"right"},{file:c.edit,side:"middle"}]).find(v=>v.file.type=="Unsupported");if(d==null)return null;if(d.file.type!="Unsupported")throw new Error("this statement is unreachable; this check exists to make TS happy");return cr`error: ${d.file.value} (occurred on the +\f\r"'\`<>=]|("|')|))|$)`,"g"),zo=/'/g,Uo=/"/g,Jo=/^(?:script|style|textarea|title)$/i,Sf=O=>(b,...T)=>({_$litType$:O,strings:b,values:T}),cr=Sf(1),xi=Symbol.for("lit-noChange"),Ye=Symbol.for("lit-nothing"),Go=new WeakMap,dr=gr.createTreeWalker(gr,129);function jo(O,b){if(!Array.isArray(O)||!O.hasOwnProperty("raw"))throw Error("invalid template strings array");return Po!==void 0?Po.createHTML(b):b}const Lf=(O,b)=>{const T=O.length-1,N=[];let M,K=b===2?"":"",h=wi;for(let c=0;c"?(h=M??wi,w=-1):m[1]===void 0?w=-2:(w=h.lastIndex-m[2].length,v=m[1],h=m[3]===void 0?hr:m[3]==='"'?Uo:zo):h===Uo||h===zo?h=hr:h===Bo||h===Ro?h=wi:(h=hr,M=void 0);const _=h===hr&&O[c+1].startsWith("/>")?" ":"";K+=h===wi?d+wf:w>=0?(N.push(v),d.slice(0,w)+qo+d.slice(w)+qt+_):d+qt+(w===-2?c:_)}return[jo(O,K+(O[T]||"")+(b===2?"":"")),N]};class Ai{constructor({strings:b,_$litType$:T},N){let M;this.parts=[];let K=0,h=0;const c=b.length-1,d=this.parts,[v,m]=Lf(b,T);if(this.el=Ai.createElement(v,N),dr.currentNode=this.el.content,T===2){const w=this.el.content.firstChild;w.replaceWith(...w.childNodes)}for(;(M=dr.nextNode())!==null&&d.length0){M.textContent=cn?cn.emptyScript:"";for(let _=0;_2||N[0]!==""||N[1]!==""?(this._$AH=Array(N.length-1).fill(new String),this.strings=N):this._$AH=Ye}_$AI(b,T=this,N,M){const K=this.strings;let h=!1;if(K===void 0)b=Rr(this,b,T,0),h=!ki(b)||b!==this._$AH&&b!==xi,h&&(this._$AH=b);else{const c=b;let d,v;for(b=K[0],d=0;d{const N=(T==null?void 0:T.renderBefore)??b;let M=N._$litPart$;if(M===void 0){const K=(T==null?void 0:T.renderBefore)??null;N._$litPart$=M=new _i(b.insertBefore(Ti(),K),K,void 0,T??{})}return M._$AI(O),M};function _f(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function xl(O,b=!1){const T=_f(),N=`_${T}`;return Object.defineProperty(window,N,{value:M=>(b&&Reflect.deleteProperty(window,N),O==null?void 0:O(M)),writable:!1,configurable:!0}),T}async function Vo(O,b={}){return new Promise((T,N)=>{const M=xl(h=>{T(h),Reflect.deleteProperty(window,`_${K}`)},!0),K=xl(h=>{N(h),Reflect.deleteProperty(window,`_${M}`)},!0);window.__TAURI_IPC__({cmd:O,callback:M,error:K,...b})})}async function Al(O){return Vo("tauri",O)}async function Df(O=0){return Al({__tauriModule:"Process",message:{cmd:"exit",exitCode:O}})}function Tl(O){return O.type=="Text"?O.value:null}const Ml="__TAURI__"in globalThis;async function es(O,b,T){if(Ml){let N={};return T!=null&&(N={result:T}),await Vo(O,N)}else return await ts(O,b,T)}async function ts(O,b,T){let N=null,M={};T!=null&&(N=JSON.stringify(T),M["Content-Type"]="application/json");const K=await fetch(`/api/${O}`,{method:b,body:N,headers:M});if(K.ok)return await K.json();{let h="";throw K.status<500&&(h=`Likely bug in the webapp: got response "${K.status} ${K.statusText}" for "${O}" request. Additional details, if any, follow. `),h+await K.text()}}async function _l(O){Ml?await Df(O):await ts("exit","POST",O)}async function Nf(){await _l(0)}async function Ko(){await _l(1)}async function Of(){await _l(2)}async function $o(O){return await es("save","PUT",O)}async function Ef(){const O=await es("get_merge_data","GET");for(const b in O)O[b]={left:O[b][0],right:O[b][1],edit:O[b][2]};return O}class Wf{constructor(b){Eo(this,"merge_views");this.merge_views=b}values(){const b={};for(const T in this.merge_views)b[T]=this.merge_views[T].editor().getValue();return b}}function Ff(O,b){let T=[],N=c=>`${c}_${O}`,M=c=>{let d=Array.from([{file:c.left,side:"left"},{file:c.right,side:"right"},{file:c.edit,side:"middle"}]).find(v=>v.file.type=="Unsupported");if(d==null)return null;if(d.file.type!="Unsupported")throw new Error("this statement is unreachable; this check exists to make TS happy");return cr`error: ${d.file.value} (occurred on the ${d.side} side)`};for(const c in b){const d=M(b[c]);d!=null?T.push(cr`
${c}: ${d} -
`); - } else { - templates.push(html` -
- - - ${k} - - - - - -
-
- `); - } - } - - const target_element = document.getElementById(unique_id)!; - target_element.innerHTML = ""; - lit_html_render(html`${templates}`, target_element); - - let merge_views: Record = {}; - for (let k in merge_input) { - if (to_error(merge_input[k]) != null) { - continue; - } - const collapseButtonEl = document.getElementById(`collapse_${k_uid(k)}`)!; - const linewrapButtonEl = document.getElementById(`linewrap_${k_uid(k)}`)!; - const prevChangeButtonEl = document.getElementById(`prevChange_${k_uid(k)}`)!; - const nextChangeButtonEl = document.getElementById(`nextChange_${k_uid(k)}`)!; - const detailsButtonEl = ( - document.getElementById(`details_${k_uid(k)}`)! - ); - const cmEl = document.getElementById(`cm_${k_uid(k)}`)!; - - const config = { - value: to_text(merge_input[k].edit) ?? "", - origLeft: to_text(merge_input[k].left) ?? "", // Set to null for 2 panes - orig: to_text(merge_input[k].right) ?? "", - lineNumbers: true, - /* TODO: Toggling line wrapping breaks `collapseIdentical`. Need a - settings system where the user can decide whether they want line wrapping, - save, and reload. */ - lineWrapping: false, - mode: "text/plain", - connect: "align", - collapseIdentical: true, - }; - const merge_view = CodeMirror.MergeView(cmEl, config); - merge_view.editor().setOption("extraKeys", { - "Alt-Down": cm_nextChange, - "Option-Down": cm_nextChange, - "Cmd-Down": cm_nextChange, - "Alt-Up": cm_prevChange, - "Option-Up": cm_prevChange, - "Cmd-Up": cm_prevChange, - Tab: cm_nextChange, - }); - collapseButtonEl.onclick = () => cm_collapseSame(merge_view.editor()); - linewrapButtonEl.onclick = () => cm_toggleLineWrapping(merge_view.editor()); - prevChangeButtonEl.onclick = () => cm_prevChange(merge_view.editor()); - nextChangeButtonEl.onclick = () => cm_nextChange(merge_view.editor()); - // Starting with details closed breaks CodeMirror, especially line numbers - // in left and right merge view. - detailsButtonEl.open = false; - detailsButtonEl.ontoggle = () => merge_view.editor().refresh(); - console.log(detailsButtonEl); - - // TODO: Resizing. See https://codemirror.net/5/demo/merge.html - merge_views[k] = merge_view; - } - - return new MergeState(merge_views); -} - -function cm_collapseSame(cm: any) { - // console.log(cm.getOption("collapseIdentical")); - cm.setOption( - /* TODO: Doesn't seem to work. Might need to recreate the whole editor */ - "collapseIdentical", - !cm.getOption("collapseIdentical") - ); - cm.setValue(cm.getValue()); - console.log(cm.getOption("collapseIdentical")); - cm.scrollIntoView(null, 50); -} - -function cm_toggleLineWrapping(cm: any) { - cm.setOption( - /* TODO: Interferes with collapseIdentical, always moves cursor to beginning */ - "lineWrapping", - !cm.getOption("lineWrapping") - ); - cm.setValue(cm.getValue()); - // cm.scrollIntoView(null, 50); // Always happens automatically -} - -function cm_nextChange(cm: Editor) { - cm.execCommand("goNextDiff"); - cm.scrollIntoView(null, 50); -} -function cm_prevChange(cm: Editor) { - cm.execCommand("goPrevDiff"); - cm.scrollIntoView(null, 50); -} +import {render_input} from "./merge_state"; // Error handling function show_error_to_user(e: any) { diff --git a/webapp/src/merge_state.ts b/webapp/src/merge_state.ts new file mode 100644 index 0000000..802ecec --- /dev/null +++ b/webapp/src/merge_state.ts @@ -0,0 +1,183 @@ +import { html, render as lit_html_render } from "lit-html"; + +import CodeMirror, { Editor } from "codemirror"; +import { MergeView } from "codemirror/addon/merge/merge"; + +import { + MergeInput, + SingleFileMergeInput, + to_text, +} from "./backend_interactions"; + +class MergeState { + merge_views: Record; + + constructor(merge_views: Record) { + this.merge_views = merge_views; + } + + values(): Record { + const result: Record = {}; + for (const k in this.merge_views) { + // TODO: Treat deleted values properly + result[k] = this.merge_views[k].editor().getValue(); + } + return result; + } +} + +// TODO: Split off drawing one editor. Only draw a single div in a loop. +// Or not? Is it reasonable to render lit-html in an element that was just rendered in lit-html? +// If not, could have two functions. +// Or just don't use `lit` for creating the divs in a loop; leave a comment instead. +// +/// Renders the input inside the HTML element with id `unique_id`. +export function render_input(unique_id: string, merge_input: MergeInput) { + let templates = []; + let k_uid = (k: string) => `${k}_${unique_id}`; + let to_error = (input: SingleFileMergeInput) => { + let unsupported_value = Array.from([ + { file: input.left, side: "left" }, + { file: input.right, side: "right" }, + { file: input.edit, side: "middle" }, + ]).find((v) => v.file.type == "Unsupported"); + if (unsupported_value == null) { + return null; + } else if (unsupported_value.file.type != "Unsupported") { + throw new Error("this statement is unreachable; this check exists to make TS happy"); + } + return html`error: ${unsupported_value.file.value} (occurred on the + ${unsupported_value.side} side)`; + }; + + for (const k in merge_input) { + const error = to_error(merge_input[k]); + if (error != null) { + templates.push(html`
+ ${k}: ${error} + +
`); + } else { + templates.push(html` +
+ + + ${k} + + + + + +
+
+ `); + } + } + + const target_element = document.getElementById(unique_id)!; + target_element.innerHTML = ""; + lit_html_render(html`${templates}`, target_element); + + let merge_views: Record = {}; + for (let k in merge_input) { + if (to_error(merge_input[k]) != null) { + continue; + } + const collapseButtonEl = document.getElementById(`collapse_${k_uid(k)}`)!; + const linewrapButtonEl = document.getElementById(`linewrap_${k_uid(k)}`)!; + const prevChangeButtonEl = document.getElementById(`prevChange_${k_uid(k)}`)!; + const nextChangeButtonEl = document.getElementById(`nextChange_${k_uid(k)}`)!; + const detailsButtonEl = ( + document.getElementById(`details_${k_uid(k)}`)! + ); + const cmEl = document.getElementById(`cm_${k_uid(k)}`)!; + + const config = { + value: to_text(merge_input[k].edit) ?? "", + origLeft: to_text(merge_input[k].left) ?? "", // Set to null for 2 panes + orig: to_text(merge_input[k].right) ?? "", + lineNumbers: true, + /* TODO: Toggling line wrapping breaks `collapseIdentical`. Need a + settings system where the user can decide whether they want line wrapping, + save, and reload. */ + lineWrapping: false, + mode: "text/plain", + connect: "align", + collapseIdentical: true, + }; + const merge_view = CodeMirror.MergeView(cmEl, config); + merge_view.editor().setOption("extraKeys", { + "Alt-Down": cm_nextChange, + "Option-Down": cm_nextChange, + "Cmd-Down": cm_nextChange, + "Alt-Up": cm_prevChange, + "Option-Up": cm_prevChange, + "Cmd-Up": cm_prevChange, + Tab: cm_nextChange, + }); + collapseButtonEl.onclick = () => cm_collapseSame(merge_view.editor()); + linewrapButtonEl.onclick = () => cm_toggleLineWrapping(merge_view.editor()); + prevChangeButtonEl.onclick = () => cm_prevChange(merge_view.editor()); + nextChangeButtonEl.onclick = () => cm_nextChange(merge_view.editor()); + // Starting with details closed breaks CodeMirror, especially line numbers + // in left and right merge view. + detailsButtonEl.open = false; + detailsButtonEl.ontoggle = () => merge_view.editor().refresh(); + console.log(detailsButtonEl); + + // TODO: Resizing. See https://codemirror.net/5/demo/merge.html + merge_views[k] = merge_view; + } + + return new MergeState(merge_views); +} + +function cm_collapseSame(cm: any) { + // console.log(cm.getOption("collapseIdentical")); + cm.setOption( + /* TODO: Doesn't seem to work. Might need to recreate the whole editor */ + "collapseIdentical", + !cm.getOption("collapseIdentical") + ); + cm.setValue(cm.getValue()); + console.log(cm.getOption("collapseIdentical")); + cm.scrollIntoView(null, 50); +} + +function cm_toggleLineWrapping(cm: any) { + cm.setOption( + /* TODO: Interferes with collapseIdentical, always moves cursor to beginning */ + "lineWrapping", + !cm.getOption("lineWrapping") + ); + cm.setValue(cm.getValue()); + // cm.scrollIntoView(null, 50); // Always happens automatically +} + +function cm_nextChange(cm: Editor) { + cm.execCommand("goNextDiff"); + cm.scrollIntoView(null, 50); +} +function cm_prevChange(cm: Editor) { + cm.execCommand("goPrevDiff"); + cm.scrollIntoView(null, 50); +}