Merge branch 'stable-3.12'

* stable-3.12:
  Preserve line terminator upon edits
  Build: Sort loads
  Resize CodeMirror dynamically
  Remove outline on textbox
  Display cursor position in the bottom
  Fix going to line
  Use Postgresql, PLSQL and Cassandra from sql package
  Allow jsx to be highlighted in .js
  Allow highlighting tsx files
  Create a gerrit theme for CodeMirror
  Revert "Use json from legacy-modes"

Change-Id: Ib354054b33cfda73e092a349d9cb2afeddd32c20
diff --git a/web/BUILD b/web/BUILD
index b8f7ae6..50c2836 100644
--- a/web/BUILD
+++ b/web/BUILD
@@ -1,6 +1,6 @@
-load("//tools/js:eslint.bzl", "plugin_eslint")
-load("//tools/bzl:js.bzl", "gerrit_js_bundle")
 load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
+load("//tools/bzl:js.bzl", "gerrit_js_bundle")
+load("//tools/js:eslint.bzl", "plugin_eslint")
 
 package_group(
     name = "visibility",
diff --git a/web/element/codemirror-element.ts b/web/element/codemirror-element.ts
index d608598..386aa67 100644
--- a/web/element/codemirror-element.ts
+++ b/web/element/codemirror-element.ts
@@ -57,8 +57,15 @@
   @query('#wrapper')
   wrapper!: HTMLElement;
 
+  @query('#result')
+  result!: HTMLElement;
+
+  private lineTerminator?: string;
+
   private initialized = false;
 
+  private onResize: (() => void) | null = null;
+
   static override get styles() {
     return [
       css`
@@ -75,12 +82,36 @@
           font-family: 'Roboto Mono', 'SF Mono', 'Lucida Console', Monaco,
             monospace;
         }
+        #statusLine {
+          position: absolute;
+          bottom: 0;
+          left: 0;
+          width: 100%;
+          background-color: #f7f7f7;
+          border-top: 1px solid #ddd;
+          border-right: 1px solid #ddd;
+        }
+        #statusLine div {
+          height: inherit;
+        }
+        .cursorPosition {
+          display: inline-block;
+          margin: 0 5px 0 35px;
+          white-space: nowrap;
+        }
       `,
     ];
   }
 
   override render() {
-    return html`<div id="wrapper"></div>`;
+    return html`
+      <div id="wrapper"></div>
+      <div class="statusLine">
+        <div class="cursorPosition">
+          <span id="result"></span>
+        </div>
+      </div>
+    `;
   }
 
   override updated() {
@@ -93,20 +124,14 @@
     if (this.initialized) return;
     this.initialized = true;
 
-    const offsetTop = this.getBoundingClientRect().top;
-    const clientHeight = window.innerHeight ?? document.body.clientHeight;
-    // We are setting a fixed height, because for large files we want to
-    // benefit from CodeMirror's virtual scrolling.
-    // 80px is roughly the size of the bottom margins plus the footer height.
-    // This ensures the height of the textarea doesn't push out of screen.
-    const height = clientHeight - offsetTop - 80;
+    this.lineTerminator = this.fileContent?.includes('\r\n') ? '\r\n' : '\n';
 
     const editor = new EditorView({
       state: EditorState.create({
         doc: this.fileContent ?? '',
         extensions: [
           ...extensions(
-            height,
+            this.calculateHeight(),
             this.prefs,
             this.fileType,
             this.fileContent ?? '',
@@ -130,7 +155,7 @@
             if (update.docChanged) {
               this.dispatchEvent(
                 new CustomEvent('content-change', {
-                  detail: {value: update.state.doc.toString()},
+                  detail: {value: update.state.doc.toJSON().join(this.lineTerminator)},
                   bubbles: true,
                   composed: true,
                 })
@@ -154,6 +179,11 @@
               }
             },
           }),
+          EditorView.updateListener.of(update => {
+            if (update.selectionSet) {
+              this.updateCursorPosition(update.view);
+            }
+          }),
         ],
       }),
       parent: this.wrapper as Element,
@@ -162,9 +192,65 @@
     editor.focus();
 
     if (this.lineNum) {
-      // We have to take away one from the line number,
-      // ... because CodeMirror's line count is zero-based.
-      editor.dispatch({selection: {anchor: this.lineNum - 1}});
+      this.setCursorToLine(editor, this.lineNum);
+    }
+
+    // Makes sure to show line number and column number on initial
+    // load.
+    this.updateCursorPosition(editor);
+
+    this.onResize = () => this.updateEditorHeight(editor);
+    window.addEventListener('resize', this.onResize);
+  }
+
+  override disconnectedCallback() {
+    super.disconnectedCallback();
+    if (this.onResize) {
+      window.removeEventListener('resize', this.onResize);
+      this.onResize = null;
+    }
+  }
+
+  setCursorToLine(view: EditorView, lineNum: number) {
+    const totalLines = view.state.doc.lines;
+    // If you try going to a line that does not exist
+    // codemirror will error out. Instead lets just log.
+    // Line 1 will be selected automatically.
+    if (lineNum < 1 || lineNum > totalLines) {
+      console.warn(`Line number ${lineNum} is out of bounds (valid range: 1 - ${totalLines}).`);
+      return;
+    }
+
+    const line = view.state.doc.line(lineNum);
+    view.dispatch({
+      selection: { anchor: line.from },
+      scrollIntoView: true
+    });
+    view.focus();
+  }
+
+  private updateCursorPosition(view: EditorView) {
+    const cursor = view.state.selection.main.head;
+    const line = view.state.doc.lineAt(cursor);
+    if (this.result) {
+      this.result.textContent = `Line: ${line.number}, Column: ${cursor - line.from + 1}`;
+    }
+  }
+
+  private calculateHeight() {
+    const offsetTop = this.getBoundingClientRect().top;
+    const clientHeight = window.innerHeight ?? document.body.clientHeight;
+    // We take offsetTop twice to ensure the height of the texarea doesn't push
+    // out of screen. We no longer do a hardcore value which was 80 before.
+    // offsetTop seems to be what we've been looking for to do it dynamically.
+    return Math.max(0, clientHeight - offsetTop - offsetTop);
+  }
+
+  private updateEditorHeight(editor: EditorView) {
+    const height = this.calculateHeight();
+    const editorElement = editor.dom;
+    if (editorElement) {
+      editorElement.style.height = `${height}px`;
     }
   }
 }
diff --git a/web/element/extensions.ts b/web/element/extensions.ts
index 5d0593a..6c2be75 100644
--- a/web/element/extensions.ts
+++ b/web/element/extensions.ts
@@ -15,13 +15,13 @@
 } from '@codemirror/view';
 import {EditorState, Extension} from '@codemirror/state';
 import {
-  defaultHighlightStyle,
   indentOnInput,
   foldGutter,
   foldKeymap,
   bracketMatching,
   syntaxHighlighting,
   indentUnit,
+  HighlightStyle,
 } from '@codemirror/language';
 import {
   defaultKeymap,
@@ -34,6 +34,7 @@
 import {rulerPlugin} from './ruler';
 import {language} from './language';
 import {EditPreferencesInfo} from './codemirror-element';
+import {tags as t} from '@lezer/highlight';
 
 const colorTheme = (dark: boolean) =>
   EditorView.theme(
@@ -46,6 +47,9 @@
         color: 'var(--deemphasized-text-color)',
         'background-color': 'var(--background-color-secondary)',
       },
+      '&.cm-editor.cm-focused': {
+        outline: 'none'
+      },
     },
     {dark}
   );
@@ -98,6 +102,43 @@
     },
   });
 
+export const gerritTheme = HighlightStyle.define([
+  {tag: [t.standard(t.tagName), t.tagName], color: 'var(--syntax-tag-color)'},
+  {tag: t.comment, color: 'var(--syntax-comment-color)'},
+  {tag: [t.className, t.propertyName], color: 'var(--syntax-attr-color)'},
+  {tag: t.variableName, color: 'var(--syntax-variable-color)'},
+  {tag: [t.attributeName, t.operator], color: 'var(--syntax-attr-color)'},
+  {tag: t.number, color: 'var(--syntax-number-color)'},
+  {tag: t.keyword, color: 'var(--syntax-keyword-color)'},
+  {tag: t.typeName, color: 'var(--syntax-type-color)'},
+  {tag: t.typeOperator, color: 'blue'},
+  {tag: t.string, color: 'var(--syntax-string-color)'},
+  {tag: t.regexp, color: 'var(--syntax-regexp-color)'},
+  {tag: t.meta, color: 'var(--syntax-meta-color)'},
+  {tag: [t.name, t.quote], color: 'var(--syntax-title-color)'},
+  {tag: t.definition(t.variableName), color: '#00f'},
+  {tag: t.local(t.variableName), color: '#30a'},
+  {tag: t.definition(t.propertyName), color: '#00c'},
+  {
+    tag: [t.heading, t.strong],
+    color: 'var(--syntax-strong-color)',
+    fontWeight: 'bold',
+  },
+  {tag: t.emphasis, fontStyle: 'italic'},
+  {tag: t.deleted, color: '#a11'},
+  {tag: t.inserted, color: '#164'},
+  {tag: t.literal, color: 'var(--syntax-literal-color)'},
+  {
+    tag: [t.atom, t.bool, t.special(t.variableName)],
+    color: 'var(--syntax-literal-color)',
+  },
+  {tag: [t.url, t.escape, t.link], color: 'var(--syntax-string-color)'},
+  {tag: t.special(t.string), color: '#e40'},
+  {tag: t.link, textDecoration: 'underline'},
+  {tag: t.strikethrough, textDecoration: 'line-through'},
+  {tag: t.invalid, color: '#f00'},
+]);
+
 export const extensions = (
   height: number,
   prefs?: EditPreferencesInfo,
@@ -151,7 +192,7 @@
   if (prefs.syntax_highlighting && language(fileType)) {
     codeExtensions.push(
       language(fileType) as Extension,
-      syntaxHighlighting(defaultHighlightStyle, {fallback: true})
+      syntaxHighlighting(gerritTheme, {fallback: true})
     );
   }
 
diff --git a/web/element/language.ts b/web/element/language.ts
index ba76215..c8a492c 100644
--- a/web/element/language.ts
+++ b/web/element/language.ts
@@ -46,7 +46,6 @@
 import {http} from '@codemirror/legacy-modes/mode/http';
 import {idl} from '@codemirror/legacy-modes/mode/idl';
 import {jinja2} from '@codemirror/legacy-modes/mode/jinja2';
-import {json} from '@codemirror/legacy-modes/mode/javascript';
 import {jsonld} from '@codemirror/legacy-modes/mode/javascript';
 import {julia} from '@codemirror/legacy-modes/mode/julia';
 import {kotlin} from '@codemirror/legacy-modes/mode/clike';
@@ -82,7 +81,6 @@
 import {sparql} from '@codemirror/legacy-modes/mode/sparql';
 import {spreadsheet} from '@codemirror/legacy-modes/mode/spreadsheet';
 import {solr} from '@codemirror/legacy-modes/mode/solr';
-import {pgSQL, plSQL, cassandra} from '@codemirror/legacy-modes/mode/sql';
 import {squirrel} from '@codemirror/legacy-modes/mode/clike';
 import {stex} from '@codemirror/legacy-modes/mode/stex';
 import {swift} from '@codemirror/legacy-modes/mode/swift';
@@ -111,13 +109,14 @@
 import {html} from '@codemirror/lang-html';
 import {java} from '@codemirror/lang-java';
 import {javascript} from '@codemirror/lang-javascript';
+import {json} from '@codemirror/lang-json';
 import {less} from '@codemirror/lang-less';
 import {markdown} from '@codemirror/lang-markdown';
 import {php} from '@codemirror/lang-php';
 import {python} from '@codemirror/lang-python';
 import {rust} from '@codemirror/lang-rust';
 import {sass} from '@codemirror/lang-sass';
-import {sql} from '@codemirror/lang-sql';
+import {Cassandra, PLSQL, PostgreSQL, sql} from '@codemirror/lang-sql';
 import {vue} from '@codemirror/lang-vue';
 import {xml} from '@codemirror/lang-xml';
 import {yaml} from '@codemirror/lang-yaml';
@@ -161,7 +160,7 @@
     case 'text/x-cmake':
       return StreamLanguage.define(cmake);
     case 'application/json':
-      return StreamLanguage.define(json);
+      return json();
     case 'text/x-cobol':
       return StreamLanguage.define(cobol);
     case 'text/x-coffeescript':
@@ -182,11 +181,11 @@
     case 'text/x-gss':
       return StreamLanguage.define(gss);
     case 'text/x-cassandra':
-      return StreamLanguage.define(cassandra);
+      return sql({dialect: Cassandra});
     case 'text/x-pgsql':
-      return StreamLanguage.define(pgSQL);
+      return sql({dialect: PostgreSQL});
     case 'text/x-plsql':
-      return StreamLanguage.define(plSQL);
+      return sql({dialect: PLSQL});
     case 'application/x-cypher-query':
       return StreamLanguage.define(cypher);
     case 'text/x-d':
@@ -213,11 +212,10 @@
     case 'text/x-ruby':
       return StreamLanguage.define(ruby);
     case 'text/javascript':
-      return javascript();
-    case 'text/x-erlang':
-      return StreamLanguage.define(erlang);
     case 'text/jsx':
       return javascript({jsx: true});
+    case 'text/x-erlang':
+      return StreamLanguage.define(erlang);
     case 'text/x-spreadsheet':
       return StreamLanguage.define(spreadsheet);
     case 'text/x-fortran':
@@ -345,6 +343,8 @@
       return StreamLanguage.define(toml);
     case 'application/typescript':
       return javascript({typescript: true});
+    case 'text/tsx':
+      return javascript({jsx: true, typescript: true});
     case 'text/x-ttcn':
       return StreamLanguage.define(ttcn);
     case 'text/turtle':