Skip to content

Commit

Permalink
Code-editor Tabs & Console
Browse files Browse the repository at this point in the history
  • Loading branch information
nighca committed Dec 24, 2024
1 parent ac88d02 commit 92f6d0b
Show file tree
Hide file tree
Showing 36 changed files with 747 additions and 372 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ defineProps<{
opacity: 0;
color: var(--ui-color-grey-100);
border-radius: 50%;
background-color: var(--ui-color-green-200);
background-color: var(--ui-color-green-main);
}
.name {
Expand Down
12 changes: 8 additions & 4 deletions spx-gui/src/components/common/markdown-vue/MarkdownView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,18 @@ function renderHastNode(node: hast.Node, components: Components, key?: string |
function renderHastElement(element: hast.Element, components: Components, key?: string | number): VNode {
let props: Record<string, string | number | boolean>
let type: string | Component
let children: VRendered | (() => VRendered)
let children: VRendered | (() => VRendered) | undefined
const customComponents = components.custom ?? {}
if (Object.prototype.hasOwnProperty.call(customComponents, element.tagName)) {
type = customComponents[element.tagName]
props = hastProps2VueProps(element.properties)
// Use function slot for custom components to avoid Vue warning:
// [Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.
children = () => element.children.map((c, i) => renderHastNode(c, components, i))
if (element.children.length === 0) {
children = undefined
} else {
// Use function slot for custom components to avoid Vue warning:
// [Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.
children = () => element.children.map((c, i) => renderHastNode(c, components, i))
}
} else if (
// Render code blocks with `components.codeBlock`
// TODO: It may be simpler to recognize & process code blocks based on mdast instead of hast
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function initMonaco(
{ token: 'string', foreground: color.green[300] },
{ token: 'operator', foreground: color.blue.main },
{ token: 'number', foreground: color.blue[600] },
{ token: 'keyword', foreground: color.red[300] },
{ token: 'keyword', foreground: color.red[600] },
{ token: 'typeKeywords', foreground: color.purple.main },
{ token: 'brackets', foreground: color.title }
],
Expand Down
76 changes: 76 additions & 0 deletions spx-gui/src/components/editor/code-editor/CodeLink.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<script setup lang="ts">
import { computed, useSlots } from 'vue'
import { useI18n } from '@/utils/i18n'
import { type Range, type Position, type TextDocumentIdentifier, textDocumentId2CodeFileName } from './common'
import { useCodeEditorCtx } from './context'
const props = defineProps<{
file: TextDocumentIdentifier
position?: Position
range?: Range
}>()
const slots = useSlots()
const i18n = useI18n()
const codeEditorCtx = useCodeEditorCtx()
const codeFileName = computed(() => i18n.t(textDocumentId2CodeFileName(props.file)))
const defaultText = computed(() => {
const { position, range } = props
if (position != null)
return i18n.t({
en: `${codeFileName.value}: Line ${position.line} Col ${position.column}`,
zh: `${codeFileName.value}: 第 ${position.line} 行 第 ${position.column} 列`
})
if (range != null) {
const { start, end } = range
if (start.line === end.line) {
return i18n.t({
en: `${codeFileName.value}: Line ${start.line} Col ${start.column}-${end.column}`,
zh: `${codeFileName.value}: 第 ${start.line} 行 第 ${start.column}-${end.column} 列`
})
} else {
return i18n.t({
en: `${codeFileName.value}: Line ${start.line}-${end.line}`,
zh: `${codeFileName.value}: 第 ${start.line}-${end.line} 行`
})
}
}
throw new Error('Either `position` or `range` must be provided')
})
function handleClick() {
const ui = codeEditorCtx.getAttachedUI()
if (ui == null) return
const { file, position, range } = props
if (position != null) {
ui.open(file, position)
return
}
if (range != null) {
ui.open(file, range)
return
}
throw new Error('Either `position` or `range` must be provided')
}
</script>

<template>
<a class="code-link" href="javascript:;" @click.prevent="handleClick">
<template v-if="!!slots.default">
<slot></slot>
</template>
<template v-else>
{{ defaultText }}
</template>
</a>
</template>

<style lang="scss" scoped>
@import '@/components/ui/link.scss';
.code-link {
@include link(boring);
}
</style>
5 changes: 5 additions & 0 deletions spx-gui/src/components/editor/code-editor/code-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,11 @@ export class CodeEditor extends Disposable {
if (idx !== -1) this.uis.splice(idx, 1)
}

getAttachedUI() {
if (this.uis.length === 0) return null
return this.uis[this.uis.length - 1]
}

init() {
this.lspClient.init()
}
Expand Down
15 changes: 15 additions & 0 deletions spx-gui/src/components/editor/code-editor/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,3 +537,18 @@ export function textDocumentId2ResourceModelId(
}
return null
}

export function textDocumentIdEq(a: TextDocumentIdentifier | null, b: TextDocumentIdentifier | null) {
if (a == null || b == null) return a === b
return a.uri === b.uri
}

export function textDocumentId2CodeFileName(id: TextDocumentIdentifier) {
const codeFilePath = getCodeFilePath(id.uri)
if (stageCodeFilePaths.includes(codeFilePath)) {
return { en: 'Stage', zh: '舞台' }
} else {
const spriteName = codeFilePath.replace(/\.spx$/, '')
return { en: spriteName, zh: spriteName }
}
}
5 changes: 5 additions & 0 deletions spx-gui/src/components/editor/code-editor/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { CodeEditor } from './code-editor'
export type CodeEditorCtx = {
attachUI(ui: ICodeEditorUI): void
detachUI(ui: ICodeEditorUI): void
getAttachedUI(): ICodeEditorUI | null
getMonaco(): Monaco
getTextDocument: (id: TextDocumentIdentifier) => TextDocument | null
formatTextDocument(id: TextDocumentIdentifier): Promise<void>
Expand Down Expand Up @@ -159,6 +160,10 @@ export function useProvideCodeEditorCtx(
if (editorRef.value == null) throw new Error('Code editor not initialized')
editorRef.value.detachUI(ui)
},
getAttachedUI() {
if (editorRef.value == null) throw new Error('Code editor not initialized')
return editorRef.value.getAttachedUI()
},
getMonaco() {
if (monacoRef.value == null) throw new Error('Monaco not initialized')
return monacoRef.value
Expand Down
55 changes: 53 additions & 2 deletions spx-gui/src/components/editor/code-editor/ui/CodeEditorUI.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ import CopilotUI from './copilot/CopilotUI.vue'
import DiagnosticsUI from './diagnostics/DiagnosticsUI.vue'
import ResourceReferenceUI from './resource-reference/ResourceReferenceUI.vue'
import ContextMenuUI from './context-menu/ContextMenuUI.vue'
import DocumentTabs from './document-tab/DocumentTabs.vue'
import ZoomControl from './ZoomControl.vue'
const props = defineProps<{
codeFilePath: string
Expand Down Expand Up @@ -117,13 +119,17 @@ const uiRef = computed(() => {
)
})
const monacoEditorOptions: monaco.editor.IStandaloneEditorConstructionOptions = {
const initialFontSize = 12
const fontSize = useLocalStorage('spx-gui-code-font-size', initialFontSize)
const monacoEditorOptions = computed<monaco.editor.IStandaloneEditorConstructionOptions>(() => ({
language: 'spx',
theme,
tabSize,
insertSpaces,
fontSize: fontSize.value,
contextmenu: false
}
}))
const monacEditorInitDataRef = shallowRef<MonacoEditorInitData | null>(null)
Expand All @@ -141,6 +147,13 @@ watch(
signal.throwIfAborted()
ui.init(...initData)
ui.editor.onDidChangeConfiguration((e) => {
const fontSizeId = ui.monaco.editor.EditorOption.fontSize
if (e.hasChanged(fontSizeId)) {
fontSize.value = ui.editor.getOptions().get(fontSizeId)
}
})
codeEditorCtx.attachUI(ui)
signal.addEventListener('abort', () => {
codeEditorCtx.detachUI(ui)
Expand Down Expand Up @@ -200,6 +213,19 @@ watchEffect((onCleanup) => {
)
signal.addEventListener('abort', endResizing)
})
function zoomIn() {
uiRef.value.editor.trigger('keyboard', `editor.action.fontZoomIn`, {})
}
function zoomOut() {
uiRef.value.editor.trigger('keyboard', `editor.action.fontZoomOut`, {})
}
function zoomReset() {
uiRef.value.editor.updateOptions({ fontSize: initialFontSize })
uiRef.value.editor.trigger('keyboard', `editor.action.fontZoomReset`, {})
}
</script>

<template>
Expand Down Expand Up @@ -241,6 +267,10 @@ watchEffect((onCleanup) => {
<DiagnosticsUI :controller="uiRef.diagnosticsController" />
<ResourceReferenceUI :controller="uiRef.resourceReferenceController" />
<ContextMenuUI :controller="uiRef.contextMenuController" />
<aside class="right-sidebar">
<DocumentTabs class="document-tabs" />
<ZoomControl class="zoom-control" @in="zoomIn" @out="zoomOut" @reset="zoomReset" />
</aside>
</div>
</template>

Expand Down Expand Up @@ -323,10 +353,31 @@ watchEffect((onCleanup) => {
.monaco-editor {
flex: 1 1 0;
min-width: 0;
margin: 12px 0;
}
:global(.code-editor-content-widget) {
z-index: 10; // Ensure content widget is above other elements, especially cursor
padding: 2px 0; // Gap between content widget and text
}
.right-sidebar {
padding: 12px 8px;
flex: 0 0 auto;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 40px;
.document-tabs {
flex: 0 1 auto;
min-height: 0;
}
.zoom-control {
flex: 0 0 auto;
}
}
</style>
51 changes: 51 additions & 0 deletions spx-gui/src/components/editor/code-editor/ui/ZoomControl.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script setup lang="ts">
import iconZoomIn from './icons/zoom-in.svg?raw'
import iconZoomOut from './icons/zoom-out.svg?raw'
import iconZoomReset from './icons/zoom-reset.svg?raw'
const emit = defineEmits<{
in: []
out: []
reset: []
}>()
</script>

<template>
<div class="zoomer">
<!-- eslint-disable vue/no-v-html -->
<button class="zoom-btn" title="Zoom in" @click="emit('in')" v-html="iconZoomIn" />
<button class="zoom-btn" title="Zoom out" @click="emit('out')" v-html="iconZoomOut" />
<button class="zoom-btn" title="Reset" @click="emit('reset')" v-html="iconZoomReset" />
</div>
</template>

<style lang="scss" scoped>
.zoomer {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.zoom-btn {
width: 40px;
height: 40px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border: none;
cursor: pointer;
border-radius: 12px;
color: var(--ui-color-text);
background: none;
transition: background-color 0.2s;
&:hover {
background-color: var(--ui-color-grey-300);
}
&:active {
background-color: var(--ui-color-grey-400);
}
}
</style>
Loading

0 comments on commit 92f6d0b

Please sign in to comment.