Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

multi buffer support in editor #65

Draft
wants to merge 72 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
c96693a
Multi-buffer editor PoC
insmac Sep 30, 2022
f1cb613
pass-through prop
insmac Sep 30, 2022
606db6b
Add file rename
insmac Sep 30, 2022
737af34
simplify editor tabs code
insmac Sep 30, 2022
e1507e7
Cleanup renaming
insmac Oct 1, 2022
90c125b
Merge branch 'web-console/multi-buffer' of https://github.com/questdb…
insmac Oct 1, 2022
98c1f9f
Cleanup
insmac Oct 1, 2022
e8fc587
Update value on rename
insmac Oct 1, 2022
88fe488
Merge branch 'main' into web-console/multi-buffer
argshook Oct 6, 2022
af41142
add uuid
argshook Oct 6, 2022
615c02c
rename `file` -> `buffer` and use id instead of label
argshook Oct 6, 2022
196bf4b
extract label to component
argshook Oct 10, 2022
65e0038
set value to buffers
argshook Oct 10, 2022
772b3fa
add dexie and dexie-react-hooks
argshook Oct 10, 2022
f7f3534
use dexie for buffers storage
argshook Oct 10, 2022
3dcd6f6
fix everything
argshook Oct 10, 2022
05094fe
remove uuid and @types/uuid
argshook Oct 10, 2022
094330c
save activeBufferId to db
argshook Oct 11, 2022
346ec50
set active buffer after deleting one
argshook Oct 11, 2022
772fa7a
populate buffer from local storage
argshook Oct 11, 2022
f7663f9
remove query_text after popoulating indexedDB
argshook Oct 11, 2022
44d2e32
refactor editor provider
argshook Oct 12, 2022
769cd84
remove getValue
argshook Oct 12, 2022
8fd0890
refactor Monaco file
argshook Oct 12, 2022
e4a4e2b
extract language addons to file
argshook Oct 12, 2022
c238fd3
extract legacy event bus action to dedicated file
argshook Oct 12, 2022
b58a0e0
save editorViewState to indexedDB
argshook Oct 12, 2022
5bbf93d
remove localStorage preferences
argshook Oct 12, 2022
18ef1ca
fix default editorViewState
argshook Oct 12, 2022
f962535
preserve viewState when switching tabs
argshook Oct 12, 2022
b4aa8bc
add hack to restoreViewState on initial load
argshook Oct 13, 2022
625dcb3
wrap EditorProvider right in Editor component
argshook Oct 13, 2022
5f795d7
refactor restoreViewState hack
argshook Oct 13, 2022
143176c
fix buffer deleting and state save
argshook Oct 13, 2022
d264f09
fix types and rename editorReadyHook -> editorReadyTrigger
argshook Oct 13, 2022
5c917ed
fix tab renaming
argshook Oct 13, 2022
582a965
add new buffer when `?query` is used
argshook Oct 13, 2022
f9592cd
fix `executeQuery` for `addBuffer`
argshook Oct 13, 2022
cd6b652
use `addBuffer` for example quries
argshook Oct 13, 2022
4a84c2a
done check for `buffer.value`
argshook Oct 13, 2022
3d04f1b
fix `add` button in schema
argshook Oct 14, 2022
18a40ed
Merge branch 'main' into web-console/multi-buffer
argshook Oct 17, 2022
cdc5b0f
Merge branch 'main' into web-console/multi-buffer
argshook Oct 24, 2022
966ef6a
add keyboard shortcuts
argshook Oct 24, 2022
5ab9325
add getAll to buffers store API
argshook Oct 26, 2022
4a2817b
make sure at least one buffer is available
argshook Oct 26, 2022
c9dab7f
prevent closing tab with alt+w when only one in total
argshook Oct 26, 2022
a4b138a
fix buffer deletion and state save
argshook Oct 26, 2022
9063116
show `close` context menu entry only when buffers.length > 1
argshook Oct 26, 2022
25c0616
disallow renaming tab to empty string
argshook Oct 26, 2022
c1ff7ee
fix browser-tests to work with multi buffers
argshook Oct 26, 2022
78d5033
for QueryPicker add new buffer if current non empty
argshook Oct 26, 2022
7770082
upgrade cypress to v10
argshook Nov 7, 2022
41e49f9
migrate cypress config to v10
argshook Nov 7, 2022
c9b0e9a
add few browser tests for tabs
argshook Nov 7, 2022
0f9c5bc
add another test
argshook Nov 7, 2022
8601d48
Merge branch 'main' into web-console/multi-buffer
argshook Nov 7, 2022
3534e97
fix keyboard shortcut test
argshook Nov 7, 2022
c4c4bfe
do not set activebuffer for already active buffer
argshook Nov 7, 2022
b27eda6
remove `double click to rename` feature
argshook Nov 7, 2022
ac321d5
fix bug found by tabs test
argshook Nov 8, 2022
35130c7
fix active tab highlight when removing all with keyboard shortcut
argshook Nov 8, 2022
de32874
add license banner
argshook Nov 8, 2022
4188e1d
wip tab UI and scrolling
argshook Nov 9, 2022
ee6b6b6
scroll into view active tab
argshook Nov 9, 2022
7451e98
keep focus on editor after closing tab
argshook Nov 9, 2022
891c442
keep editor focus when clicking active tab
argshook Nov 9, 2022
a627d96
adjust tab style
argshook Nov 9, 2022
7760ed0
ensure tab text is not trimmed vertically
argshook Nov 9, 2022
9cb44fa
add tab open/close shortcuts info
argshook Nov 14, 2022
bb3d046
Merge branch 'main' into web-console/multi-buffer
argshook Feb 15, 2023
929560f
Merge branch 'main' into web-console/multi-buffer
bluestreak01 Feb 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
Binary file not shown.
Binary file not shown.
5 changes: 4 additions & 1 deletion packages/web-console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"compare-versions": "^5.0.1",
"core-js": "^3.22.8",
"date-fns": "2.14.0",
"dexie": "^3.2.2",
"dexie-react-hooks": "^1.1.1",
"docsearch.js": "2.6.3",
"dotenv": "^10.0.0",
"echarts": "^5.2.2",
Expand All @@ -53,7 +55,8 @@
"sql-formatter": "^4.0.2",
"styled-components": "5.1.1",
"styled-icons": "10.2.1",
"throttle-debounce": "2.2.1"
"throttle-debounce": "2.2.1",
"usehooks-ts": "^2.7.1"
},
"devDependencies": {
"@babel/cli": "^7.17.10",
Expand Down
182 changes: 159 additions & 23 deletions packages/web-console/src/providers/EditorProvider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,179 @@ import React, {
MutableRefObject,
PropsWithChildren,
useContext,
useEffect,
useRef,
useState,
} from "react"
import { editor } from "monaco-editor"
import { Monaco } from "@monaco-editor/react"
import {
insertTextAtCursor,
appendQuery,
QuestDBLanguageName,
} from "../../scenes/Editor/Monaco/utils"
import { fallbackBuffer, makeBuffer, bufferStore } from "../../store/buffers"
import { db } from "../../store/db"
import type { Buffer } from "../../store/buffers"

import { useLiveQuery } from "dexie-react-hooks"

type IStandaloneCodeEditor = editor.IStandaloneCodeEditor

type ContextProps = {
editorRef: MutableRefObject<IStandaloneCodeEditor | null> | null
monacoRef: MutableRefObject<Monaco | null> | null
export type EditorContext = {
editorRef: MutableRefObject<IStandaloneCodeEditor | null>
monacoRef: MutableRefObject<Monaco | null>
insertTextAtCursor: (text: string) => void
getValue: () => void
appendQuery: (query: string) => void
buffers: Buffer[]
activeBuffer: Buffer
setActiveBuffer: (buffer: Buffer) => Promise<void>
addBuffer: (
buffer?: Partial<Buffer>,
options?: { shouldSelectAll?: boolean },
) => Promise<Buffer>
deleteBuffer: (id: number) => Promise<void>
updateBuffer: (id: number, buffer?: Partial<Buffer>) => Promise<void>
editorReadyTrigger: (editor: IStandaloneCodeEditor) => void
}

const defaultValues = {
editorRef: null,
monacoRef: null,
insertTextAtCursor: (text: string) => undefined,
getValue: () => undefined,
appendQuery: (query: string) => undefined,
editorRef: { current: null },
monacoRef: { current: null },
insertTextAtCursor: () => undefined,
appendQuery: () => undefined,
buffers: [],
activeBuffer: fallbackBuffer,
setActiveBuffer: () => Promise.resolve(),
addBuffer: () => Promise.resolve(fallbackBuffer),
deleteBuffer: () => Promise.resolve(),
updateBuffer: () => Promise.resolve(),
editorReadyTrigger: () => undefined,
}

const EditorContext = createContext<ContextProps>(defaultValues)
const EditorContext = createContext<EditorContext>(defaultValues)

export const EditorProvider = ({ children }: PropsWithChildren<{}>) => {
const editorRef = useRef<IStandaloneCodeEditor | null>(null)
const monacoRef = useRef<Monaco | null>(null)

/*
To avoid re-rendering components that subscribe to this context
we don't set value via a useState hook
*/
const getValue = () => {
return editorRef.current?.getValue()
const editorRef = useRef<IStandaloneCodeEditor>(null)
const monacoRef = useRef<Monaco>(null)
const buffers = useLiveQuery(bufferStore.getAll, [])
const activeBufferId = useLiveQuery(
() => bufferStore.getActiveId(),
[],
)?.value

const [activeBuffer, setActiveBufferState] = useState<Buffer>(fallbackBuffer)

const ranOnce = useRef(false)
// this effect should run only once, after mount and after `buffers` and `activeBufferId` are ready from the db
useEffect(() => {
if (!ranOnce.current && buffers && activeBufferId) {
const buffer =
buffers?.find((buffer) => buffer.id === activeBufferId) ?? buffers[0]
setActiveBufferState(buffer)
ranOnce.current = true
}
}, [buffers, activeBufferId])

if (!buffers || !activeBufferId || activeBuffer === fallbackBuffer) {
return null
}

const setActiveBuffer = async (buffer: Buffer) => {
if (await bufferStore.getById(activeBuffer.id as number)) {
// check if buffer with activeBuffer.id exists, otherwise we might save editor state of a
// buffer which is being deleted
await updateBuffer(activeBuffer.id as number)
}
await bufferStore.setActiveId(buffer.id as number)
setActiveBufferState(buffer)
editorRef.current?.focus()
if (editorRef.current && monacoRef.current) {
const model = monacoRef.current?.editor.createModel(
buffer.value,
QuestDBLanguageName,
)
editorRef.current.setModel(model)
}
if (buffer.editorViewState) {
editorRef.current?.restoreViewState(buffer.editorViewState)
}
}

const addBuffer: EditorContext["addBuffer"] = async (
newBuffer,
{ shouldSelectAll = false } = {},
) => {
const currentDefaultTabNumbers = (
await db.buffers
.filter((buffer) => buffer.label.startsWith(fallbackBuffer.label))
.toArray()
)
.map((buffer) =>
buffer.label.slice(fallbackBuffer.label.length + /* whitespace */ 1),
)
.filter(Boolean)
.map((n) => parseInt(n, 10))
.sort()

const nextNumber = () => {
for (let i = 0; i <= currentDefaultTabNumbers.length; i++) {
const nextNumber = i + 1
if (!currentDefaultTabNumbers.includes(nextNumber)) {
return nextNumber
}
}
}

const buffer = makeBuffer({
...newBuffer,
label: newBuffer?.label ?? `${fallbackBuffer.label} ${nextNumber()}`,
})
const id = await db.buffers.add(buffer)
await setActiveBuffer(buffer)
if (
editorRef.current &&
monacoRef.current &&
typeof buffer.value === "string"
) {
const model = monacoRef.current?.editor.createModel(
buffer.value,
QuestDBLanguageName,
)
editorRef.current.setModel(model)

if (shouldSelectAll) {
editorRef.current?.setSelection(model.getFullModelRange())
}
}

return { id, ...buffer }
}

const updateBuffer: EditorContext["updateBuffer"] = async (id, payload) => {
const editorViewState = editorRef.current?.saveViewState()
await bufferStore.update(id, {
...payload,
...(editorViewState ? { editorViewState } : {}),
})
}

const deleteBuffer: EditorContext["deleteBuffer"] = async (id) => {
await bufferStore.delete(id)

// set new active buffer only when removing currently active buffer
const activeBufferId = (await bufferStore.getActiveId())?.value
if (typeof activeBufferId !== "undefined" && activeBufferId === id) {
const nextActive = await db.buffers.toCollection().last()
await setActiveBuffer(nextActive ?? fallbackBuffer)
}
}

return (
<EditorContext.Provider
value={{
editorRef,
monacoRef,
getValue,
insertTextAtCursor: (text) => {
if (editorRef?.current) {
insertTextAtCursor(editorRef.current, text)
Expand All @@ -60,13 +186,23 @@ export const EditorProvider = ({ children }: PropsWithChildren<{}>) => {
appendQuery(editorRef.current, text)
}
},
buffers,
activeBuffer,
setActiveBuffer,
addBuffer,
deleteBuffer,
updateBuffer,
editorReadyTrigger: (editor) => {
editor.focus()
if (activeBuffer.editorViewState) {
editor.restoreViewState(activeBuffer.editorViewState)
}
},
}}
>
{children}
</EditorContext.Provider>
)
}

export const useEditor = () => {
return useContext(EditorContext)
}
export const useEditor = () => useContext(EditorContext)
10 changes: 0 additions & 10 deletions packages/web-console/src/providers/LocalStorageProvider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ const defaultConfig: LocalConfig = {
editorLine: 10,
isNotificationEnabled: true,
notificationDelay: 5,
queryText: "",
editorSplitterBasis: 350,
resultsSplitterBasis: 350,
exampleQueriesVisited: false,
Expand All @@ -54,7 +53,6 @@ type ContextProps = {
editorLine: number
notificationDelay: number
isNotificationEnabled: boolean
queryText: string
editorSplitterBasis: number
resultsSplitterBasis: number
updateSettings: (key: StoreKey, value: SettingsType) => void
Expand All @@ -67,7 +65,6 @@ const defaultValues: ContextProps = {
editorLine: 1,
isNotificationEnabled: true,
notificationDelay: 5,
queryText: "",
editorSplitterBasis: 350,
resultsSplitterBasis: 350,
updateSettings: (key: StoreKey, value: SettingsType) => undefined,
Expand Down Expand Up @@ -100,9 +97,6 @@ export const LocalStorageProvider = ({
defaultConfig.notificationDelay,
),
)
const [queryText, setQueryText] = useState<string>(
getValue(StoreKey.QUERY_TEXT),
)
const [editorSplitterBasis, seteditorSplitterBasis] = useState<number>(
parseInteger(
getValue(StoreKey.EDITOR_SPLITTER_BASIS),
Expand Down Expand Up @@ -150,9 +144,6 @@ export const LocalStorageProvider = ({
parseInteger(value, defaultConfig.notificationDelay),
)
break
case StoreKey.QUERY_TEXT:
setQueryText(value)
break
case StoreKey.EDITOR_SPLITTER_BASIS:
seteditorSplitterBasis(
parseInteger(value, defaultConfig.editorSplitterBasis),
Expand All @@ -174,7 +165,6 @@ export const LocalStorageProvider = ({
editorLine,
isNotificationEnabled,
notificationDelay,
queryText,
editorSplitterBasis,
resultsSplitterBasis,
updateSettings,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export type LocalConfig = {
editorLine: number
notificationDelay: number
isNotificationEnabled: boolean
queryText: string
editorSplitterBasis: number
resultsSplitterBasis: number
exampleQueriesVisited: boolean
Expand Down
Loading