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

Tabs for code-editor & Console #1168

Merged
merged 1 commit into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Copy link
Collaborator Author

@nighca nighca Dec 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

原来的 code-editor/ui/markdown/CodeLink.vue 拆成了两个:

  1. code-editor/CodeLink.vue,可用于其他界面模块(如 Console)复用
  2. code-editor/ui/markdown/CodeLink.ts,基于前者实现,对外提供 string props 方便在 markdown 内容中嵌入使用

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
Loading