Merge pull request #2931 from beekeeper-studio/use-ui-kit-sqltexteditor

Use UI Kit SQL Text Editor in the main app
This commit is contained in:
Mohammad Azmi
2025-04-02 19:07:36 +07:00
committed by GitHub
12 changed files with 195 additions and 205 deletions

View File

@@ -1,3 +1,4 @@
@import "@beekeeperstudio/ui-kit/style.css";
@import "codemirror/lib/codemirror";
@import "codemirror/addon/hint/show-hint";
@import "noty/lib/noty";

View File

@@ -130,8 +130,11 @@
}
.CodeMirror {
padding-bottom: 0.25rem;
.BksSqlTextEditor {
min-height: 0;
.CodeMirror {
padding-bottom: 0.25rem;
}
}
.CodeMirror-linenumbers {

View File

@@ -1,77 +1,78 @@
.CodeMirror {
font-family: $font-family-mono, monospace;
background: $query-editor-bg!important;
color: $text-dark!important;
padding: 0.75rem 0;
z-index: 1;
height: 100%;
}
.CodeMirror-code {
line-height: 1.3!important;
}
.CodeMirror-gutters {
border: 0!important;
background: $query-editor-bg!important;
}
.CodeMirror-foldmarker {
color: $brand-primary;
text-shadow: none;
}
// Monokai
// ---------------------------------------
.cm-s-monokai {
span.cm-keyword {
color: $brand-pink;
:not(.BksTextEditor) {
.CodeMirror {
font-family: $font-family-mono, monospace;
background: $query-editor-bg!important;
color: $text-dark!important;
padding: 0.75rem 0;
z-index: 1;
height: 100%;
}
span.cm-variable-2 {
color: $brand-secondary;
.CodeMirror-code {
line-height: 1.3!important;
}
div.CodeMirror-selected {
background: rgba($theme-base, 0.25);
.CodeMirror-gutters {
border: 0!important;
background: $query-editor-bg!important;
}
.CodeMirror-linenumber {
color: rgba($theme-base, 0.15);
}
.CodeMirror-line .cm-number,
.CodeMirror-line .cm-string {
.CodeMirror-foldmarker {
color: $brand-primary;
text-shadow: none;
}
.CodeMirror-cursor {
border-color: $text-dark;
}
span.cm-bracket {
color: $text;
}
}
// Addon: Hints
// --------------------------------------
.CodeMirror-hints {
font-family: $font-family-mono, monospace;
background: lighten($theme-bg, 5%);
color: $text-dark;
padding: $gutter-h 0;
border: 0;
border-radius: 6px;
@include card-shadow;
li {
&:hover {
background: rgba($theme-base, 0.05);
// Monokai
// ---------------------------------------
.cm-s-monokai {
span.cm-keyword {
color: $brand-pink;
}
&.CodeMirror-hint-active {
background: darken($theme-secondary, 25%);
color: white;
span.cm-variable-2 {
color: $brand-secondary;
}
div.CodeMirror-selected {
background: rgba($theme-base, 0.25);
}
.CodeMirror-linenumber {
color: rgba($theme-base, 0.15);
}
.CodeMirror-line .cm-number,
.CodeMirror-line .cm-string {
color: $brand-primary;
}
.CodeMirror-cursor {
border-color: $text-dark;
}
span.cm-bracket {
color: $text;
}
}
}
.CodeMirror-hint {
padding: ($gutter-h * 0.5) $gutter-h;
border-radius: 0;
color: $text-dark;
cursor: pointer;
}
.CodeMirror-scrollbar-filler {
background-color: transparent;
// Addon: Hints
// --------------------------------------
.CodeMirror-hints {
font-family: $font-family-mono, monospace;
background: lighten($theme-bg, 5%);
color: $text-dark;
padding: $gutter-h 0;
border: 0;
border-radius: 6px;
@include card-shadow;
li {
&:hover {
background: rgba($theme-base, 0.05);
}
&.CodeMirror-hint-active {
background: darken($theme-secondary, 25%);
color: white;
}
}
}
.CodeMirror-hint {
padding: ($gutter-h * 0.5) $gutter-h;
border-radius: 0;
color: $text-dark;
cursor: pointer;
}
.CodeMirror-scrollbar-filler {
background-color: transparent;
}
}

View File

@@ -117,3 +117,25 @@ $row-handle-selected: lighten($query-editor-bg, 10%);
}
}
}
.BksTextEditor {
--bks-text-editor-bg-color: #{$query-editor-bg};
--bks-text-editor-fg-color: #{$text-dark};
--bks-text-editor-gutter-bg-color: #{$query-editor-bg};
--bks-text-editor-keyword-fg-color: #{$brand-pink};
--bks-text-editor-variable-2-fg-color: #{$brand-secondary};
--bks-text-editor-selected-bg-color: #{rgba($theme-base, 0.25)};
--bks-text-editor-linenumber-fg-color: #{rgba($theme-base, 0.15)};
--bks-text-editor-number-fg-color: #{$brand-primary};
--bks-text-editor-string-fg-color: #{$brand-primary};
--bks-text-editor-cursor-bg-color: #{$text-dark};
--bks-text-editor-selected-bg-color: #{rgba($theme-base, 0.25)};
}
.BksTextEditor-hints {
--bks-text-editor-context-menu-bg-color: #{lighten($theme-bg, 5%)};
--bks-text-editor-context-menu-fg-color: #{$text-dark};
--bks-text-editor-context-menu-item-bg-color-active: #{darken($theme-secondary, 25%)};
--bks-text-editor-context-menu-item-bg-color-hover: #{rgba($theme-base, 0.05)};
--bks-text-editor-context-menu-item-fg-color-active: #ffffff;
}

View File

@@ -580,3 +580,7 @@ x-switch {
background: rgba(darken($theme-bg, 8%), 0.5);
}
}
.BksTextEditor {
font-weight: 500;
}

View File

@@ -482,59 +482,23 @@ x-buttons {
}
}
.BksTextEditor {
--bks-text-editor-string-fg-color: #{$brand-success};
--bks-text-editor-number-fg-color: #{$brand-warning};
--bks-text-editor-keyword-fg-color: #{$brand-pink};
--bks-text-editor-variable-2-fg-color: #{$brand-secondary};
--bks-text-editor-selected-bg-color: #{rgba($theme-base, 0.25)};
--bks-text-editor-linenumber-fg-color: #{rgba($theme-base, 0.25)};
--bks-text-editor-cursor-bg-color: #{$text-dark};
--bks-text-editor-bracket-fg-color: #{$text};
// CodeMirror
// ----------------------------------
.CodeMirror-hints {
background: darken($theme-bg, 5%)!important;
li {
&.CodeMirror-hint-active {
background: $brand-info;
color: white;
}
}
}
.cm-s-monokai {
span {
.cm-string {
color: $brand-success;
}
}
.CodeMirror-line {
.cm-number {
color: $brand-warning;
}
}
}
.cm-s-monokai {
font-weight: 500;
span.cm-keyword {
color: $brand-pink;
}
span.cm-variable-2 {
color: $brand-secondary;
}
div.CodeMirror-selected {
background: rgba($theme-secondary, 0.25);
}
.CodeMirror-linenumber {
color: rgba($theme-base, 0.25);
}
.CodeMirror-line .cm-number {
color: $brand-warning;
}
.CodeMirror-line .cm-string {
color: $brand-success;
}
.CodeMirror-cursor {
border-color: $text-dark;
}
span.cm-bracket {
color: $text;
}
}
.BksTextEditor-hints {
--bks-text-editor-context-menu-bg-color: #{darken($theme-bg, 5%)};
--bks-text-editor-context-menu-item-bg-color-active: #{$brand-info};
--bks-text-editor-context-menu-item-fg-color-active: #ffffff;
}
// Noty

View File

@@ -32,24 +32,33 @@
</div>
</div>
<sql-text-editor
v-model="unsavedText"
v-bind.sync="editor"
:value="unsavedText"
:height="editor.height"
:read-only="editor.readOnly"
:focus="focusingElement === 'text-editor'"
@update:focus="updateTextEditorFocus"
:markers="editorMarkers"
:connection-type="connectionType"
:extra-keybindings="keybindings"
:formatter-dialect="formatterDialect"
:identifier-dialect="identifierDialect"
:keybindings="keybindings"
:vim-config="vimConfig"
@initialized="handleEditorInitialized"
:keymap="userKeymap"
:entities="entities"
:columns-getter="columnsGetter"
:default-schema="defaultSchema"
:mode="dialectData.textEditorMode"
@bks-initialized="handleEditorInitialized"
@bks-value-change="unsavedText = $event.value"
@bks-blur="onTextEditorBlur?.()"
@bks-query-selection-change="handleQuerySelectionChange"
/>
<span class="expand" />
<div class="toolbar text-right">
<div
class="toolbar text-right"
ref="toolbar"
>
<div class="editor-help expand" />
<div class="expand" />
<div
class="actions btn-group"
ref="actions"
>
<div class="actions btn-group">
<x-button
v-if="showDryRun"
class="btn btn-flat btn-small dry-run-btn"
@@ -323,12 +332,11 @@
import { mapGetters, mapState } from 'vuex'
import { identify } from 'sql-query-identifier'
import { splitQueries, isTextSelected } from '../lib/db/sql_tools'
import { EditorMarker } from '@/lib/editor/utils'
import ProgressBar from './editor/ProgressBar.vue'
import ResultTable from './editor/ResultTable.vue'
import ShortcutHints from './editor/ShortcutHints.vue'
import SQLTextEditor from '@/components/common/texteditor/SQLTextEditor.vue'
import SqlTextEditor from "@beekeeperstudio/ui-kit/vue/sql-text-editor"
import QueryEditorStatusBar from './editor/QueryEditorStatusBar.vue'
import rawlog from '@bksLogger'
@@ -338,6 +346,10 @@
import { PropType } from 'vue'
import { TransportOpenTab, findQuery } from '@/common/transport/TransportOpenTab'
import { blankFavoriteQuery } from '@/common/transport'
import { TableOrView } from "@/lib/db/models";
import { FormatterDialect, dialectFor } from "@shared/lib/dialects/models"
import { findSqlQueryIdentifierDialect } from "@/lib/editor/CodeMirrorPlugins";
import { registerQueryMagic } from "@/lib/editor/CodeMirrorPlugins";
const log = rawlog.scope('query-editor')
const isEmpty = (s) => _.isEmpty(_.trim(s))
@@ -345,7 +357,7 @@
export default {
// this.queryText holds the current editor value, always
components: { ResultTable, ProgressBar, ShortcutHints, QueryEditorStatusBar, ErrorAlert, MergeManager, SqlTextEditor: SQLTextEditor },
components: { ResultTable, ProgressBar, ShortcutHints, QueryEditorStatusBar, ErrorAlert, MergeManager, SqlTextEditor },
props: {
tab: Object as PropType<TransportOpenTab>,
active: Boolean
@@ -375,7 +387,6 @@
tableHeight: 0,
savePrompt: false,
lastWord: null,
marker: null,
queryParameterValues: {},
queryForExecution: null,
executeTime: 0,
@@ -395,6 +406,9 @@
*/
focusElement: 'none',
focusingElement: 'none',
individualQueries: [],
currentlySelectedQuery: null,
}
},
computed: {
@@ -469,43 +483,6 @@
result() {
return this.results[this.selectedResult]
},
individualQueries() {
if (!this.unsavedText) return []
return splitQueries(this.unsavedText, this.identifyDialect)
},
currentlySelectedQueryIndex() {
const queries = this.individualQueries
for (let i = 0; i < queries.length; i++) {
// Find a query in between anchor and head cursors
if (this.editor.cursorIndex !== this.editor.cursorIndexAnchor) {
const isSelected = isTextSelected(queries[i].start, queries[i].end, this.editor.cursorIndexAnchor, this.editor.cursorIndex)
if (isSelected) return i
}
// Otherwise, find a query that sits before the cursor
else if (this.editor.cursorIndex <= queries[i].end + 1) return i
}
return null
},
currentlySelectedQuery() {
if (this.currentlySelectedQueryIndex === null) return null
return this.individualQueries[this.currentlySelectedQueryIndex]
},
currentQueryPosition() {
if(!this.editor.initialized || !this.currentlySelectedQuery || !this.individualQueries) {
return null
}
const qi = this.currentlySelectedQueryIndex
const previousQuery = qi === 0 ? null : this.individualQueries[qi - 1]
// adding 1 to account for semicolon
const start = previousQuery ? previousQuery.end + 1: 0
const end = this.currentlySelectedQuery.end
return {
from: start,
to: end + 1
}
},
rowCount() {
return this.result && this.result.rows ? this.result.rows.length : 0
},
@@ -602,13 +579,21 @@
},
editorMarkers() {
const markers = []
if (this.marker) markers.push(this.marker)
if (this.errorMarker) markers.push(this.errorMarker)
return markers
},
showResultTable() {
return this.rowCount > 0
},
entities() {
return this.tables.map((t: TableOrView) => ({ schema: t.schema, name: t.name }))
},
formatterDialect() {
return FormatterDialect(dialectFor(this.connectionType))
},
identifierDialect() {
return findSqlQueryIdentifierDialect(this.connectionType)
},
},
watch: {
error() {
@@ -659,24 +644,6 @@
this.$modal.hide(`save-modal-${this.tab.id}`)
}
},
currentQueryPosition() {
this.marker = null
if(!this.individualQueries || this.individualQueries.length < 2) {
return;
}
if (!this.currentQueryPosition) {
return
}
const { from, to } = this.currentQueryPosition
const editorText = this.unsavedText
// const lines = editorText.split(/\n/)
const [markStart, markEnd] = this.locationFromPosition(editorText, from, to)
this.marker = { from: markStart, to: markEnd, type: 'highlight' } as EditorMarker
},
async focusElement(element, oldElement) {
if (oldElement === 'text-editor' && element !== 'text-editor') {
await this.blurTextEditor()
@@ -748,7 +715,16 @@
})
})
},
handleEditorInitialized() {
handleEditorInitialized(detail) {
this.editor.initialized = true
detail.codemirror.on("cursorActivity", (cm) => {
this.editor.selection = cm.getSelection()
this.editor.cursorIndex = cm.getDoc().indexFromPos(cm.getCursor())
});
registerQueryMagic(() => this.defaultSchema, () => this.tables, detail.codemirror)
// this gives the dom a chance to kick in and render these
// before we try to read their heights
this.$nextTick(() => {
@@ -798,7 +774,7 @@
},
updateEditorHeight() {
let height = this.$refs.topPanel.clientHeight
height -= this.$refs.actions.clientHeight
height -= this.$refs.toolbar.clientHeight
this.editor.height = height
},
triggerSave() {
@@ -995,11 +971,6 @@
this.close()
}
},
updateTextEditorFocus(focused: boolean) {
if (!focused) {
this.onTextEditorBlur?.()
}
},
async switchPaneFocus(_event?: KeyboardEvent, target?: 'text-editor' | 'table') {
if (target) {
this.focusElement = target
@@ -1033,6 +1004,29 @@
this.focusingElement = 'none'
})
},
async columnsGetter(tableName: string) {
let table = this.tables.find(
(t: TableOrView) => t.name === tableName || `${t.schema}.${t.name}` === tableName
);
if (!table) {
return null;
}
// Only refresh columns if we don't have them cached.
if (!table.columns?.length) {
await this.$store.dispatch("updateTableColumns", table);
table = this.tables.find(
(t: TableOrView) => t.name === tableName || `${t.schema}.${t.name}` === tableName
);
}
return table?.columns.map((c) => c.columnName);
},
handleQuerySelectionChange({ queries, selectedQuery }) {
this.individualQueries = queries;
this.currentlySelectedQuery = selectedQuery;
},
},
async mounted() {
if (this.shouldInitialize) this.initialize()

View File

@@ -222,7 +222,7 @@ export default {
},
hasUsedDropdown: {
get() {
return this.settings?.hideResultsDropdown.value ?? false
return this.settings?.hideResultsDropdown?.value ?? false
},
set(value) {
this.$store.dispatch('settings/save', { key: 'hideResultsDropdown', value })

View File

@@ -35,7 +35,7 @@ export function autoquoteHandler(
}
}
function findSqlQueryIdentifierDialect(connectionType: string) {
export function findSqlQueryIdentifierDialect(connectionType: string) {
const mappings = {
sqlserver: "mssql",
sqlite: "sqlite",

View File

@@ -213,7 +213,7 @@
}
}
.BksTextEditor-hints {
.BksTextEditor-hints.CodeMirror-hints {
--bks-text-editor-context-menu-bg-color: #{darken($theme-bg, 5%)};
--bks-text-editor-context-menu-fg-color: #{$text-dark};
--bks-text-editor-context-menu-item-bg-color-active: #{$brand-info};

View File

@@ -7,7 +7,7 @@
"url": "https://beekeeperstudio.io"
},
"private": false,
"version": "0.1.1",
"version": "0.1.2",
"repository": {
"type": "git",
"url": "git+https://github.com/beekeeper-studio/beekeeper-studio.git",

View File

@@ -9,10 +9,11 @@
"apps/*"
],
"scripts": {
"bks:build": "yarn workspace beekeeper-studio electron:build",
"bks:dev": "yarn workspace beekeeper-studio electron:serve",
"bks:build": "yarn lib:build && yarn workspace beekeeper-studio electron:build",
"bks:dev": "yarn lib:build && yarn workspace beekeeper-studio electron:serve",
"bks:config:build": "yarn workspace beekeeper-studio config:build",
"lib:dev": "yarn workspace @bks/ui-kit lib:dev",
"lib:dev": "yarn workspace @beekeeperstudio/ui-kit lib:dev",
"lib:build": "yarn workspace @beekeeperstudio/ui-kit build",
"sqltools:build": "yarn workspace sqltools build",
"sqltools:dev": "yarn workspace sqltools serve",
"electron:build": "yarn bks:build",