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 afa54e3
Show file tree
Hide file tree
Showing 33 changed files with 742 additions and 367 deletions.
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 afa54e3

Please sign in to comment.