diff --git a/packages/origine2/src/components/Assets/Upload.tsx b/packages/origine2/src/components/Assets/Upload.tsx index 587fceac0..de415e8d3 100644 --- a/packages/origine2/src/components/Assets/Upload.tsx +++ b/packages/origine2/src/components/Assets/Upload.tsx @@ -7,13 +7,13 @@ import {t} from '@lingui/macro'; import { List } from '@fluentui/react'; import { getFileIcon } from '@/utils/getFileIcon'; -export type IUploadProps = { +export interface IUploadProps { name?: string; className?: string; title?: string; multiple?: boolean; onChange?: ChangeEventHandler; -}; +} const ArrowUploadIcon = bundleIcon(ArrowUpload32Filled, ArrowUpload32Regular); const ClosedCaptionIcon = bundleIcon(Delete20Filled, Delete20Regular); @@ -22,16 +22,24 @@ const getFiles = (fileList?: FileList|null) => { const files = []; for (const file of fileList) { - files.push(file.name) + files.push(file.name); } return files; -} +}; + +// File item render component +const renderFileItem = (file?: string) => ( +
+ icon + {file} +
+); export default function Upload({ name, className, title, multiple, onChange }: IUploadProps) { const [files, setFiles] = useState([]); const onChangeHandler: ChangeEventHandler = (ev) => { - setFiles(getFiles(ev.target.files)) + setFiles(getFiles(ev.target.files)); onChange?.(ev); }; @@ -41,9 +49,6 @@ export default function Upload({ name, className, title, multiple, onChange }: I
{t`点击或拖拽文件至此上传`}
-
- icon - {file} -
}/> + ; } \ No newline at end of file diff --git a/packages/origine2/src/config/highlighting/hl.json b/packages/origine2/src/config/highlighting/hl.json index cc90fe19b..2a6b17341 100644 --- a/packages/origine2/src/config/highlighting/hl.json +++ b/packages/origine2/src/config/highlighting/hl.json @@ -13,40 +13,47 @@ "argument-list": { "patterns": [ { - "comment": "only one argument left, ie kwarg0=val0", - "match": "(?!.*\\s\\-).+", + "comment": ">1 arguments left, ie -kwarg0=val0 -kwarg1=val1 ...", + "match": "(\\s\\-)(.*?)(\\s\\-.*)$", "captures": { - "0": { + "1": { + "patterns": [ + { + "include": "#operator" + } + ] + }, + "2": { "patterns": [ { "include": "#parameter" } ] + }, + "3": { + "patterns": [ + { + "include": "#argument-list" + } + ] } } }, { - "comment": ">1 arguments left, ie [kwarg0=val0] -[kwarg1=val1]", - "match": "(.*?)(\\s\\-)(.*?)$", + "comment": "only one argument left, ie -kwarg0=val0", + "match": "(\\s\\-)(.*)", "captures": { "1": { - "patterns": [ - { - "include": "#parameter" - } - ] - }, - "2": { "patterns": [ { "include": "#operator" } ] }, - "3": { + "2": { "patterns": [ { - "include": "#argument-list" + "include": "#parameter" } ] } @@ -75,7 +82,7 @@ { "comment": "value only, ie val0", "match": "(?!.*\\=).+", - "name": "variable.other.webgal" + "name": "variable.parameter.webgal" }, { "comment": "name and value, ie [kwarg0]=[val0]", @@ -92,7 +99,7 @@ ] }, "3": { - "name": "variable.other.webgal" + "name": "markup.quote" } } } @@ -111,19 +118,12 @@ }, { "comment": "utterance and argument list", - "match": "(.*?)(\\s\\-)(.*?)$", + "match": "(.*?)(\\s\\-.*?)$", "captures": { "1": { "name": "string.unquoted.utterance.webgal" }, "2": { - "patterns": [ - { - "include": "#operator" - } - ] - }, - "3": { "patterns": [ { "include": "#argument-list" @@ -136,7 +136,7 @@ }, "character-colon": { "comment": "[char]:[utt[ -args]][;cmt]", - "match": "^(?!(?:intro|changeBg|changeFigure|miniAvatar|playEffect|changeScene|choose|end|bgm|playVideo|setComplexAnimation|setFilter|pixiInit|pixiPerform|label|jumpLabel|setVar|callScene|showVars|unlockCg|unlockBgm|say|filmMode|callScene|setTextbox|setAnimation)\\:)(.*?)(\\:)([^\\;\\n]*?)($|\\;.*?$)", + "match": "^(?!(?:intro|changeBg|changeFigure|miniAvatar|playEffect|changeScene|choose|end|bgm|playVideo|setComplexAnimation|setFilter|pixiInit|pixiPerform|label|jumpLabel|setVar|callScene|showVars|unlockCg|unlockBgm|say|filmMode|callScene|setTextbox|setAnimation|setTransition|setTransform|say|getUserInput|applyStyle|wait)\\:)(.*?)(\\:)([^\\;\\n]*?)($|\\;.*?$)", "captures": { "1": { "name": "meta.character.webgal", @@ -171,7 +171,7 @@ }, "command-colon": { "comment": "cmd:[arg0[ -args]][;cmt]", - "match": "^(intro|changeBg|changeFigure|playEffect|miniAvatar|changeScene|choose|end|bgm|playVideo|setComplexAnimation|setFilter|pixiInit|pixiPerform|label|jumpLabel|setVar|callScene|showVars|unlockCg|unlockBgm|say|filmMode|callScene|setTextbox|setAnimation)(\\:)([^\\;\\n]*?)($|\\;.*?$)", + "match": "^(intro|changeBg|changeFigure|playEffect|miniAvatar|changeScene|choose|end|bgm|playVideo|setComplexAnimation|setFilter|pixiInit|pixiPerform|label|jumpLabel|setVar|callScene|showVars|unlockCg|unlockBgm|say|filmMode|callScene|setTextbox|setAnimation|setTransition|setTransform|say|getUserInput|applyStyle|wait)(\\:)([^\\;\\n]*?)($|\\;.*?$)", "captures": { "1": { "name": "meta.command.webgal", @@ -206,7 +206,7 @@ }, "command-semicolon": { "comment": "cmd;[cmt]", - "match": "^(intro|changeBg|changeFigure|playEffect|miniAvatar|changeScene|choose|end|bgm|playVideo|setComplexAnimation|setFilter|pixiInit|pixiPerform|label|jumpLabel|setVar|callScene|showVars|unlockCg|unlockBgm|say|filmMode|callScene|setTextbox|setAnimation)($|\\;.*?$)", + "match": "^(intro|changeBg|changeFigure|playEffect|miniAvatar|changeScene|choose|end|bgm|playVideo|setComplexAnimation|setFilter|pixiInit|pixiPerform|label|jumpLabel|setVar|callScene|showVars|unlockCg|unlockBgm|say|filmMode|callScene|setTextbox|setAnimation|setTransition|setTransform|say|getUserInput|applyStyle|wait)($|\\;.*?$)", "captures": { "1": { "name": "meta.command.webgal", @@ -227,7 +227,7 @@ }, "utterance-semicolon": { "comment": "utt[ -args];[cmt]", - "match": "^(?!(?:intro|changeBg|changeFigure|playEffect|miniAvatar|changeScene|choose|end|bgm|playVideo|setComplexAnimation|setFilter|pixiInit|pixiPerform|label|jumpLabel|setVar|callScene|showVars|unlockCg|unlockBgm|say|filmMode|callScene|setTextbox|setAnimation)\\;)([^\\:\\;\\n]+?)(\\;.*?$)", + "match": "^(?!(?:intro|changeBg|changeFigure|playEffect|miniAvatar|changeScene|choose|end|bgm|playVideo|setComplexAnimation|setFilter|pixiInit|pixiPerform|label|jumpLabel|setVar|callScene|showVars|unlockCg|unlockBgm|say|filmMode|callScene|setTextbox|setAnimation|setTransition|setTransform|say|getUserInput|applyStyle|wait)\\;)([^\\:\\;\\n]+?)(\\;.*?$)", "captures": { "1": { "patterns": [ diff --git a/packages/origine2/src/config/themes/monokai-light-vs.json b/packages/origine2/src/config/themes/monokai-light-vs.json index f4a295d58..36305fe1e 100644 --- a/packages/origine2/src/config/themes/monokai-light-vs.json +++ b/packages/origine2/src/config/themes/monokai-light-vs.json @@ -264,7 +264,7 @@ } } ], - "semanticHighlighting": true, + "semanticHighlighting": false, "semanticTokenColors": { "variable": "#cf7000", "keyword": "#9966b8", diff --git a/packages/origine2/src/locales/en.po b/packages/origine2/src/locales/en.po index f3ce36f8d..5ed1ddff2 100644 --- a/packages/origine2/src/locales/en.po +++ b/packages/origine2/src/locales/en.po @@ -554,7 +554,7 @@ msgstr "Delete" msgid "删除属性" msgstr "Delete Property" -#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:250 +#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:251 #: src/pages/editor/GraphicalEditor/SentenceEditor/Choose.tsx:28 msgid "删除本句" msgstr "Delete this sentence" @@ -1004,7 +1004,7 @@ msgstr "Open effect editor" msgid "打开文件夹" msgstr "Open folder" -#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:258 +#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:259 msgid "执行到此句" msgstr "Execute to this sentence" @@ -1336,7 +1336,7 @@ msgstr "Unrecognized" msgid "未识别的指令" msgstr "Unrecognized command" -#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:221 +#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:222 msgid "本句前插入句子" msgstr "Insert sentence before this" @@ -1493,7 +1493,7 @@ msgstr "Add new line" msgid "添加自定义属性" msgstr "Add Custom Property" -#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:274 +#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:275 #: src/pages/editor/GraphicalEditor/SentenceEditor/Choose.tsx:63 #: src/pages/editor/Topbar/Topbar.tsx:108 msgid "添加语句" diff --git a/packages/origine2/src/locales/ja.po b/packages/origine2/src/locales/ja.po index 911b735df..423b9e9d8 100644 --- a/packages/origine2/src/locales/ja.po +++ b/packages/origine2/src/locales/ja.po @@ -554,7 +554,7 @@ msgstr "削除" msgid "删除属性" msgstr "属性を削除" -#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:250 +#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:251 #: src/pages/editor/GraphicalEditor/SentenceEditor/Choose.tsx:28 msgid "删除本句" msgstr "この文を削除" @@ -1004,7 +1004,7 @@ msgstr "エフェクトエディタを開く" msgid "打开文件夹" msgstr "フォルダーを開く" -#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:258 +#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:259 msgid "执行到此句" msgstr "この文まで実行" @@ -1336,7 +1336,7 @@ msgstr "認識されないコマンド" msgid "未识别的指令" msgstr "認識されないコマンド" -#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:221 +#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:222 msgid "本句前插入句子" msgstr "選択した文の前に追加" @@ -1493,7 +1493,7 @@ msgstr "新しい行を追加" msgid "添加自定义属性" msgstr "カスタム属性を追加" -#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:274 +#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:275 #: src/pages/editor/GraphicalEditor/SentenceEditor/Choose.tsx:63 #: src/pages/editor/Topbar/Topbar.tsx:108 msgid "添加语句" diff --git a/packages/origine2/src/locales/zhCn.po b/packages/origine2/src/locales/zhCn.po index 03dd927b7..b74f2c2d8 100644 --- a/packages/origine2/src/locales/zhCn.po +++ b/packages/origine2/src/locales/zhCn.po @@ -574,7 +574,7 @@ msgstr "删除" msgid "删除属性" msgstr "删除属性" -#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:250 +#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:251 #: src/pages/editor/GraphicalEditor/SentenceEditor/Choose.tsx:28 msgid "删除本句" msgstr "删除本句" @@ -1024,7 +1024,7 @@ msgstr "打开效果编辑器" msgid "打开文件夹" msgstr "打开文件夹" -#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:258 +#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:259 msgid "执行到此句" msgstr "执行到此句" @@ -1356,7 +1356,7 @@ msgstr "未识别" msgid "未识别的指令" msgstr "未识别的指令" -#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:221 +#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:222 msgid "本句前插入句子" msgstr "本句前插入句子" @@ -1513,7 +1513,7 @@ msgstr "添加新行" msgid "添加自定义属性" msgstr "添加自定义属性" -#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:274 +#: src/pages/editor/GraphicalEditor/GraphicalEditor.tsx:275 #: src/pages/editor/GraphicalEditor/SentenceEditor/Choose.tsx:63 #: src/pages/editor/Topbar/Topbar.tsx:108 msgid "添加语句" diff --git a/packages/origine2/src/pages/editor/GraphicalEditor/GraphicalEditor.tsx b/packages/origine2/src/pages/editor/GraphicalEditor/GraphicalEditor.tsx index 8479c6174..e277e061d 100644 --- a/packages/origine2/src/pages/editor/GraphicalEditor/GraphicalEditor.tsx +++ b/packages/origine2/src/pages/editor/GraphicalEditor/GraphicalEditor.tsx @@ -85,7 +85,7 @@ export default function GraphicalEditor(props: IGraphicalEditorProps) { function submitScene(newSentences: SentenceItem[], index: number) { const newScene = mergeToString(newSentences.map(item => item.content)); const updateIndex = index + 1; - editorLineHolder.recordSceneEdittingLine(props.targetPath, updateIndex); + editorLineHolder.recordSceneEditingLine(props.targetPath, updateIndex); api.assetsControllerEditTextFile({ textFile: newScene, @@ -138,7 +138,7 @@ export default function GraphicalEditor(props: IGraphicalEditorProps) { if (!result.destination) { return; } - editorLineHolder.recordSceneEdittingLine(props.targetPath, result.destination.index); + editorLineHolder.recordSceneEditingLine(props.targetPath, result.destination.index); reorder( result.source.index, result.destination.index @@ -148,6 +148,7 @@ export default function GraphicalEditor(props: IGraphicalEditorProps) { function syncToIndex(index: number) { const targetValue = sentenceData.value[index]?.content || ""; WsUtil.sendSyncCommand(props.targetPath, index + 1, targetValue,true); + editorLineHolder.recordSceneEditingLine(props.targetPath, index + 1); } useEffect(() => { diff --git a/packages/origine2/src/pages/editor/GraphicalEditor/SentenceEditor/ChangeFigure.tsx b/packages/origine2/src/pages/editor/GraphicalEditor/SentenceEditor/ChangeFigure.tsx index 2c876105d..eba311486 100644 --- a/packages/origine2/src/pages/editor/GraphicalEditor/SentenceEditor/ChangeFigure.tsx +++ b/packages/origine2/src/pages/editor/GraphicalEditor/SentenceEditor/ChangeFigure.tsx @@ -249,87 +249,87 @@ export default function ChangeFigure(props: ISentenceEditorProps) { title={t`效果编辑器`} sentenceIndex={props.index} bottomBarChildren={[ - -
- { - const newDuration = Number(data.value); - if (isNaN(newDuration) || data.value === '') - duration.set(""); - else - duration.set(newDuration); - }} onBlur={submit}/> -
-
, - - { - animationFlag.set(newValue?.toString() ?? ""); + +
+ { + const newDuration = Number(data.value); + if (isNaN(newDuration) || data.value === '') + duration.set(""); + else + duration.set(newDuration); + }} onBlur={submit}/> +
+
, + + { + animationFlag.set(newValue?.toString() ?? ""); + submit(); + }} + /> + , +
+ + <> + {mouthOpen.value + "\u00a0\u00a0"} + { + mouthOpen.set(fileDesc?.name ?? ""); + submit(); + }} + extName={[".png", ".jpg", ".webp"]}/> + + +
, +
+ + <> + {mouthHalfOpen.value + "\u00a0\u00a0"} + { + mouthHalfOpen.set(fileDesc?.name ?? ""); + submit(); + }} + extName={[".png", ".jpg", ".webp"]}/> + + +
, +
+ + <> + {mouthClose.value + "\u00a0\u00a0"} + { + mouthClose.set(fileDesc?.name ?? ""); + submit(); + }} + extName={[".png", ".jpg", ".webp"]}/> + + +
, +
+ + <> + {eyesOpen.value + "\u00a0\u00a0"} + { + eyesOpen.set(fileDesc?.name ?? ""); + submit(); + }} + extName={[".png", ".jpg", ".webp"]}/> + + +
, +
+ + <> + {eyesClose.value + "\u00a0\u00a0"} + { + eyesClose.set(fileDesc?.name ?? ""); submit(); }} - /> - , -
- - <> - {mouthOpen.value + "\u00a0\u00a0"} - { - mouthOpen.set(fileDesc?.name ?? ""); - submit(); - }} - extName={[".png", ".jpg", ".webp"]}/> - - -
, -
- - <> - {mouthHalfOpen.value + "\u00a0\u00a0"} - { - mouthHalfOpen.set(fileDesc?.name ?? ""); - submit(); - }} - extName={[".png", ".jpg", ".webp"]}/> - - -
, -
- - <> - {mouthClose.value + "\u00a0\u00a0"} - { - mouthClose.set(fileDesc?.name ?? ""); - submit(); - }} - extName={[".png", ".jpg", ".webp"]}/> - - -
, -
- - <> - {eyesOpen.value + "\u00a0\u00a0"} - { - eyesOpen.set(fileDesc?.name ?? ""); - submit(); - }} - extName={[".png", ".jpg", ".webp"]}/> - - -
, -
- - <> - {eyesClose.value + "\u00a0\u00a0"} - { - eyesClose.set(fileDesc?.name ?? ""); - submit(); - }} - extName={[".png", ".jpg", ".webp"]}/> - - -
, + extName={[".png", ".jpg", ".webp"]}/> + + +
, ]} > void }){ @@ -59,15 +61,15 @@ export function EffectEditor(props:{ const r = red / 255; const g = green / 255; const b = blue / 255; - const cmax = Math.max(r, g, b), cmin = Math.min(r, g, b); + const cmax = Math.max(r, g, b); const cmin = Math.min(r, g, b); let delta = cmax - cmin; let h = 0; if (delta !== 0) { - if (cmax === r) h = ((g - b) / delta) * 60; - else if (cmax === g) h = ((b - r) / delta) * 60 + 120; - else h = ((r - g) / delta) * 60 + 240; - if (h < 0) h += 360; + if (cmax === r) h = ((g - b) / delta) * 60; + else if (cmax === g) h = ((b - r) / delta) * 60 + 120; + else h = ((r - g) / delta) * 60 + 240; + if (h < 0) h += 360; } let s = (cmax === 0) ? 0 : (delta / cmax) * 100.0; @@ -83,22 +85,22 @@ export function EffectEditor(props:{ v, hex: `${red.toString(16).padStart(2, '0')}${green.toString(16).padStart(2, '0')}${blue.toString(16).padStart(2, '0')}`, str: `rgba(${red}, ${green}, ${blue}, 100)`, - } - } + }; + }; const getColor = (): IColor => { const r = colorRed.value === "" ? 255 : colorRed.value; const g = colorGreen.value === "" ? 255 : colorGreen.value; const b = colorBlue.value === "" ? 255 : colorBlue.value; return rgbColor(r, g, b); - } + }; const getBevelColor = (): IColor => { const r = bevelRed.value === "" ? 255 : bevelRed.value; const g = bevelGreen.value === "" ? 255 : bevelGreen.value; const b = bevelBlue.value === "" ? 255 : bevelBlue.value; return rgbColor(r, g, b); - } + }; const color = useValue(getColor()); const bevelColor = useValue(getBevelColor()); @@ -119,6 +121,7 @@ export function EffectEditor(props:{ bevelBlue.set(newColor.b); }; + // eslint-disable-next-line complexity const updateObject = () => { const result:{[key: string]: any;} = {}; console.log(x.value); @@ -185,51 +188,51 @@ export function EffectEditor(props:{ return <> - - { - x.set(data.value); - }} - onBlur={submit}/> - - - { - y.set(data.value); - }} - onBlur={submit}/> - - - { - rotation.set(data.value); - }} - onBlur={submit}/> - - - { - scaleX.set(data.value); - }} - onBlur={submit}/> - - - { - scaleY.set(data.value); - }} - onBlur={submit}/> - + + { + x.set(data.value); + }} + onBlur={submit}/> + + + { + y.set(data.value); + }} + onBlur={submit}/> + + + { + rotation.set(data.value); + }} + onBlur={submit}/> + + + { + scaleX.set(data.value); + }} + onBlur={submit}/> + + + { + scaleY.set(data.value); + }} + onBlur={submit}/> + @@ -277,13 +280,13 @@ export function EffectEditor(props:{ onBlur={submit}/> - { - saturation.set(data.value); - }} - onBlur={submit}/> + { + saturation.set(data.value); + }} + onBlur={submit}/> + > {t`应用颜色变化`} @@ -378,10 +381,10 @@ export function EffectEditor(props:{
diff --git a/packages/origine2/src/pages/editor/GraphicalEditor/components/OptionCategory.tsx b/packages/origine2/src/pages/editor/GraphicalEditor/components/OptionCategory.tsx index d33bb8e41..c454a8444 100644 --- a/packages/origine2/src/pages/editor/GraphicalEditor/components/OptionCategory.tsx +++ b/packages/origine2/src/pages/editor/GraphicalEditor/components/OptionCategory.tsx @@ -1,5 +1,5 @@ import {ReactNode} from "react"; -import styles from './optionCategory.module.scss' +import styles from './optionCategory.module.scss'; interface IOptionCategoryProps { title: string; @@ -8,10 +8,10 @@ interface IOptionCategoryProps { } export function OptionCategory(props: IOptionCategoryProps) { - return
-
{props.title}
-
- {props.children} -
-
; + return
+
{props.title}
+
+ {props.children} +
+
; } \ No newline at end of file diff --git a/packages/origine2/src/pages/editor/TextEditor/TextEditor.tsx b/packages/origine2/src/pages/editor/TextEditor/TextEditor.tsx index 38d6888b4..e7ef07e56 100644 --- a/packages/origine2/src/pages/editor/TextEditor/TextEditor.tsx +++ b/packages/origine2/src/pages/editor/TextEditor/TextEditor.tsx @@ -20,8 +20,6 @@ interface ITextEditorProps { isHide: boolean; } -let isAfterMount = false; - export default function TextEditor(props: ITextEditorProps) { const target = useGameEditorContext((state) => state.currentTag); const tags = useGameEditorContext((state) => state.tags); @@ -43,23 +41,42 @@ export default function TextEditor(props: ITextEditorProps) { logger.debug('脚本编辑器挂载'); lspSceneName.value = sceneName; editorRef.current = editor; + + configureMonaco(editor, monaco); + editor.onDidChangeCursorPosition(debounce((event:monaco.editor.ICursorPositionChangedEvent) => { - const lineNumber = event.position.lineNumber; + const previousCursorPosition = editorLineHolder.getScenePosition(props.targetPath); const editorValue = editor.getValue(); - const targetValue = editorValue.split('\n')[lineNumber - 1]; - // const trueLineNumber = getTrueLinenumber(lineNumber, editorRef.current?.getValue()??''); - if (!isAfterMount) { - editorLineHolder.recordSceneEdittingLine(props.targetPath, lineNumber); - } + const targetValue = editorValue.split('\n')[event.position.lineNumber - 1]; if (event.reason === monaco.editor.CursorChangeReason.Explicit) { - WsUtil.sendSyncCommand(target?.path??'', lineNumber, targetValue); + if (event.position.lineNumber !== previousCursorPosition.lineNumber) { + WsUtil.sendSyncCommand(target?.path??'', event.position.lineNumber, targetValue); + } } - }, 500)); - editor.updateOptions({ unicodeHighlight: { ambiguousCharacters: false }, wordWrap: isAutoWarp ? 'on' : 'off' }); - isAfterMount = true; + editorLineHolder.recordSceneEditingPosition(props.targetPath, event.position); + })); + editor.updateOptions({ + unicodeHighlight: { ambiguousCharacters: false }, + wordWrap: isAutoWarp ? 'on' : 'off' , + smoothScrolling: true, + }); updateEditData(); } + function configureMonaco(editor: monaco.editor.IStandaloneCodeEditor, monaco: Monaco) { + const languageConfiguration: monaco.languages.LanguageConfiguration = { + comments: { + lineComment: ";", + }, + brackets: [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ], + }; + monaco.languages.setLanguageConfiguration('webgal', languageConfiguration); + } + useEffect(() => { editorRef?.current?.updateOptions?.({ wordWrap: isAutoWarp ? 'on' : 'off' }); }, [isAutoWarp]); @@ -72,17 +89,13 @@ export default function TextEditor(props: ITextEditorProps) { const handleChange = debounce((value: string | undefined, ev: monaco.editor.IModelContentChangedEvent) => { if(!isEditorReady.value) return; logger.debug('编辑器提交更新'); - const lineNumber = ev.changes[0].range.startLineNumber; - if (!isAfterMount) { - editorLineHolder.recordSceneEdittingLine(props.targetPath, lineNumber); - } - + const previousCursorPosition = editorLineHolder.getScenePosition(props.targetPath); // const trueLineNumber = getTrueLinenumber(lineNumber, value ?? ""); if (value) currentText.value = value; eventBus.emit('update-scene', currentText.value); api.assetsControllerEditTextFile({textFile: currentText.value, path: props.targetPath}).then((res) => { - const targetValue = currentText.value.split('\n')[lineNumber - 1]; - WsUtil.sendSyncCommand(target?.path??'', lineNumber, targetValue); + const targetValue = currentText.value.split('\n')[previousCursorPosition.lineNumber - 1]; + WsUtil.sendSyncCommand(target?.path??'', previousCursorPosition.lineNumber, targetValue); }); }, 500); @@ -97,12 +110,9 @@ export default function TextEditor(props: ITextEditorProps) { eventBus.emit('update-scene', data.toString()); editorRef.current?.getModel()?.setValue(currentText.value); isEditorReady.value = true; - if (isAfterMount) { - const targetLine = editorLineHolder.getSceneLine(props.targetPath); - editorRef?.current?.setPosition({ lineNumber: targetLine, column: 0 }); - editorRef?.current?.revealLineInCenter(targetLine, 0); - isAfterMount = false; - } + const targetPosition = editorLineHolder.getScenePosition(props.targetPath); + editorRef?.current?.setPosition(targetPosition); + editorRef?.current?.revealPositionInCenterIfOutsideViewport(targetPosition, monaco.editor.ScrollType.Immediate); }); } diff --git a/packages/origine2/src/runtime/WG_ORIGINE_RUNTIME.ts b/packages/origine2/src/runtime/WG_ORIGINE_RUNTIME.ts index 5679002ac..d8eb92e77 100644 --- a/packages/origine2/src/runtime/WG_ORIGINE_RUNTIME.ts +++ b/packages/origine2/src/runtime/WG_ORIGINE_RUNTIME.ts @@ -1,3 +1,5 @@ +import {Position} from 'monaco-editor'; + export const WG_ORIGINE_RUNTIME = { textEditor:{ isInitWasm:false @@ -7,13 +9,24 @@ export const WG_ORIGINE_RUNTIME = { export const lspSceneName = {value: ""}; class EditorLineHolder{ - private mapSceneUrlToSentence = new Map(); - public recordSceneEdittingLine(sceneUrl:string,lineNumber:number){ - this.mapSceneUrlToSentence.set(sceneUrl,lineNumber); + private mapSceneUrlToSentence = new Map(); + + public recordSceneEditingLine(sceneUrl: string, lineNumber: number) { + this.mapSceneUrlToSentence.set(sceneUrl, new Position(lineNumber, 0)); + // console.log(this.mapSceneUrlToSentence); + } + + public recordSceneEditingPosition(sceneUrl: string, position: Position) { + this.mapSceneUrlToSentence.set(sceneUrl, position); // console.log(this.mapSceneUrlToSentence); } - public getSceneLine(sceneUrl:string){ - return this.mapSceneUrlToSentence.get(sceneUrl) ??0; + + public getSceneLine(sceneUrl: string): number { + return this.mapSceneUrlToSentence.get(sceneUrl)?.lineNumber ?? 0; + } + + public getScenePosition(sceneUrl: string): Position { + return this.mapSceneUrlToSentence.get(sceneUrl) ?? new Position(0, 0); } } diff --git a/packages/terre2/src/Modules/lsp/completion/commandArgs.ts b/packages/terre2/src/Modules/lsp/completion/commandArgs.ts index 708820b77..78f5cb99f 100644 --- a/packages/terre2/src/Modules/lsp/completion/commandArgs.ts +++ b/packages/terre2/src/Modules/lsp/completion/commandArgs.ts @@ -1,9 +1,4 @@ -import { - CompletionItem, - CompletionItemKind, - CompletionParams, - CompletionTriggerKind, -} from 'vscode-languageserver'; +import { MarkupContent, MarkupKind } from 'vscode-languageserver'; // FIXME: Error: Cannot find module 'webgal-parser/src/interface/sceneInterface' // import { commandType } from 'webgal-parser/src/interface/sceneInterface'; @@ -46,538 +41,9 @@ export enum commandType { applyStyle = 32, } -type ICommandArgs = { - mandatory?: (CompletionItem & { hasValue?: boolean })[]; - optional?: (CompletionItem & { hasValue?: boolean })[]; -}; - -type ICommandArgsMap = { - [ct in commandType]: ICommandArgs; -}; - -function makeDocString(descrption: string, example: string) { - let result = ''; - if (descrption !== '') { - result += `${descrption}\n`; - } - if (example !== '') { - result += `示例:\n \`\`\`\n ${example}\n \`\`\``; - } - return result; -} - -export const commandArgs: ICommandArgsMap = { - [commandType.say]: { - mandatory: [], - optional: [ - { - label: 'concat', - labelDetails: { - description: '连接对话至上一句对话之后', - }, - documentation: makeDocString( - `有时候,可能你希望在某一句对话执行到某个阶段时加入演出效果,比如切换表情等。 -这时候,你可以使用 -notend -concat 参数来实现在对话中插入任意语句。 - -concat 代表本句对话连接在上一句对话之后 - -notend 代表本句对话没有结束,在后面可能连接演出或对话。`, - `切换立绘!马上切换表情...... -notend -concat; - changeFigure:k2.png -next; - 切换表情! -concat;`, - ), - }, - { - label: 'notend', - labelDetails: { - description: '在对话后连接演出或对话', - }, - documentation: makeDocString( - `有时候,可能你希望在某一句对话执行到某个阶段时加入演出效果,比如切换表情等。 -这时候,你可以使用 -notend -concat 参数来实现在对话中插入任意语句。 - -concat 代表本句对话连接在上一句对话之后 - -notend 代表本句对话没有结束,在后面可能连接演出或对话。`, - `WebGAL:测试语句插演出!马上切换立绘...... -notend; - changeFigure:k1.png -next; - 切换立绘!马上切换表情...... -notend -concat; - `, - ), - }, - { - label: 'fontsize', - labelDetails: { - description: '文字大小', - }, - hasValue: true, - }, - ], - }, - [commandType.changeBg]: { - mandatory: [ - { - label: 'unlockname', - labelDetails: { description: '解锁CG名称' }, - hasValue: true, - }, - ], - optional: [], - }, - [commandType.changeFigure]: { - mandatory: [], - optional: [ - { - label: 'left', - labelDetails: { - description: '放置到左侧', - }, - documentation: makeDocString( - '现在,你可以在页面的三个不同位置放置不同的立绘,只需要在放置立绘的语句处加上你要放置的位置就可以了', - `changeFigure:testFigure03.png -left; - changeFigure:testFigure04.png; - changeFigure:testFigure03.png -right;`, - ), - }, - { - label: 'right', - labelDetails: { - description: '放置到右侧', - }, - documentation: makeDocString( - '现在,你可以在页面的三个不同位置放置不同的立绘,只需要在放置立绘的语句处加上你要放置的位置就可以了', - `changeFigure:testFigure03.png -left; - changeFigure:testFigure04.png; - changeFigure:testFigure03.png -right;`, - ), - }, - { - label: 'id', - labelDetails: { - description: '为立绘指定ID', - }, - documentation: makeDocString( - '如果你想要更精确地控制立绘,可以为立绘指定 id 和初始位置', - `; // 一个初始位置在右侧的自由立绘 - changeFigure:testFigure03.png -left -id=test1; - ; // 通过 id 关闭立绘 - changeFigure:none -id=test1;`, - ), - hasValue: true, - }, - ], - }, - [commandType.bgm]: { - mandatory: [], - optional: [ - { - label: 'volume', - labelDetails: { - description: '调整音量', - }, - documentation: makeDocString('', 'bgm:夏影.mp3 -volume=30;'), - hasValue: true, - }, - { - label: 'enter', - labelDetails: { - description: '进行淡入/淡出播放', - }, - documentation: makeDocString( - '', - ';淡入:\n bgm:夏影.mp3 -enter=3000;\n ;淡出:bgm:none -enter=3000;', - ), - hasValue: true, - }, - ], - }, - [commandType.video]: { - mandatory: [], - optional: [ - { label: 'skipOff', labelDetails: { description: '阻止用户跳过视频' } }, - ], - }, - [commandType.pixi]: { - mandatory: [], - optional: [], - }, - /** - * NO_ARGS - */ - [commandType.pixiInit]: { - mandatory: [], - optional: [], - }, - [commandType.intro]: { - mandatory: [], - optional: [ - { - label: 'hold', - labelDetails: { description: '结束后保持独白界面' }, - documentation: makeDocString( - '', - 'intro:回忆不需要适合的剧本,|反正一说出口,|都成了戏言。 -hold;', - ), - }, - ], - }, - /** - * NO_ARGS - */ - [commandType.miniAvatar]: { - mandatory: [], - optional: [], - }, - /** - * NO_ARGS - */ - [commandType.changeScene]: { - mandatory: [], - optional: [], - }, - /** - * NO_ARGS - */ - [commandType.choose]: { - mandatory: [], - optional: [], - }, - /** - * NO_ARGS - */ - [commandType.end]: { - mandatory: [], - optional: [], - }, - /** - * ??? - */ - [commandType.setComplexAnimation]: { - mandatory: [], - optional: [], - }, - /** - * ??? - */ - [commandType.setFilter]: { - mandatory: [], - optional: [], - }, - /** - * NO_ARGS - */ - [commandType.label]: { - mandatory: [], - optional: [], - }, - /** - * NO_ARGS - */ - [commandType.jumpLabel]: { - mandatory: [], - optional: [], - }, - /** - * ??? - */ - [commandType.chooseLabel]: { - mandatory: [], - optional: [], - }, - [commandType.setVar]: { - mandatory: [], - optional: [ - { - label: 'global', - labelDetails: { description: '长效变量(全局变量)' }, - documentation: makeDocString( - `WebGAL 的普通变量是跟随存档的,也就是说,任何变量只存在于当前的游戏场景中,只有存档才能将其保存下来,读档将其恢复。 -为了解决可能存在的作者希望设置多周目的问题,提供长效(全局)变量,一旦设置,则在整个游戏中生效,除非用户清除全部数据。`, - `jumpLabel:turn-2 -when=a>0; - setVar:a=1 -global; - 一周目; - changeScene:一周目剧情.txt; - label:turn-2; - 二周目; - changeScene:二周目剧情.txt;`, - ), - }, - ], - }, - /** - * ??? - */ - [commandType.if]: { - mandatory: [], - optional: [], - }, - /** - * NO_ARGS - */ - [commandType.callScene]: { - mandatory: [], - optional: [], - }, - /** - * ??? - */ - [commandType.showVars]: { - mandatory: [], - optional: [], - }, - [commandType.unlockCg]: { - mandatory: [ - { - label: 'name', - labelDetails: { - description: '指定CG名称', - }, - documentation: makeDocString( - '', - 'unlockCg:xgmain.jpeg -name=星光咖啡馆与死神之蝶', - ), - }, - ], - optional: [ - { - label: 'series', - labelDetails: { - description: '指定CG系列', - }, - documentation: makeDocString( - '同系列的立绘以后会合并展示(即展示成可以切换的同系列CG)', - 'unlockCg:xgmain.jpeg -name=星光咖啡馆与死神之蝶 -series=1;', - ), - }, - ], - }, - [commandType.unlockBgm]: { - mandatory: [ - { - label: 'name', - labelDetails: { - description: '指定BGM名称', - }, - documentation: makeDocString( - '', - 'unlockBgm:s_Title.mp3 -name=Smiling-Swinging!!!;', - ), - }, - ], - optional: [], - }, - /** - * ??? - */ - [commandType.filmMode]: { - mandatory: [], - optional: [], - }, - /** - * ??? - */ - [commandType.setTextbox]: { - mandatory: [], - optional: [], - }, - [commandType.setAnimation]: { - mandatory: [ - { - label: 'target', - labelDetails: { description: '作用目标' }, - documentation: makeDocString( - '', - `; // 为中间立绘设置一个从下方进入的动画,并转到下一句 - setAnimation:enter-from-bottom -target=fig-center -next;`, - ), - }, - ], - optional: [], - }, - [commandType.playEffect]: { - mandatory: [], - optional: [ - { - label: 'volume', - labelDetails: { - description: '调整音量', - }, - documentation: makeDocString( - '可以为效果音设置一个 -volume 参数,来调整它的音量。', - 'playEffect:xxx.mp3 -volume=30;', - ), - hasValue: true, - }, - { - label: 'id', - labelDetails: { - description: '设置ID', - }, - documentation: makeDocString( - '为效果音赋予一个 id 将会自动启用效果音循环,后续使用相同的 id 来停止。', - `playEffect:xxx.mp3 -id=xxx; - playEffect:none -id=xxx; // 停止这个循环的效果音`, - ), - hasValue: true, - }, - ], - }, - /** - * ??? - */ - [commandType.setTempAnimation]: { - mandatory: [], - optional: [], - }, - /** - * ??? - */ - [commandType.comment]: { - mandatory: [], - optional: [], - }, - [commandType.setTransform]: { - mandatory: [ - { - label: 'target', - labelDetails: { description: '作用目标' }, - }, - ], - optional: [], - }, - [commandType.setTransition]: { - mandatory: [ - { - label: 'target', - labelDetails: { description: '作用目标' }, - }, - { - label: 'enter', - labelDetails: { description: '进场效果' }, - }, - { - label: 'exit', - labelDetails: { description: '出场效果' }, - }, - ], - optional: [], - }, - [commandType.getUserInput]: { - mandatory: [ - { - label: 'title', - labelDetails: { - description: '提示文本', - }, - documentation: makeDocString( - '', - 'getUserInput:name -title=如何称呼你 -buttonText=确认; 将用户输入写入 name 变量中', - ), - }, - { - label: 'buttonText', - labelDetails: { - description: '确认按钮文本', - }, - documentation: makeDocString( - '', - 'getUserInput:name -title=如何称呼你 -buttonText=确认; 将用户输入写入 name 变量中', - ), - }, - ], - optional: [], - }, - /** - * ??? - */ - [commandType.applyStyle]: { - mandatory: [], - optional: [], - }, -}; - -function makeInsertText(label: string, insertDash: boolean, hasValue: boolean) { - let result = ''; - if (insertDash) { - result += '-'; - } - result += label; - if (hasValue) { - result += '='; - } - return result; -} - -function setKind(item: CompletionItem) { - return { ...item, kind: CompletionItemKind.Field }; -} - -export function makeCompletion( - args: ICommandArgs, - insertDash: boolean, -): CompletionItem[] { - let result: CompletionItem[] = []; - - result = result.concat( - args.mandatory.map((arg) => setKind({ ...arg })), - args.optional.map((arg) => - setKind({ - ...arg, - label: `${arg.label}?`, - insertText: makeInsertText(arg.label, insertDash, arg.hasValue), - }), - ), - ); - - result.push( - setKind({ - label: 'next?', - insertText: makeInsertText('next', insertDash, false) + ';', - labelDetails: { - description: '在执行完本条语句后立刻跳转到下一条语句', - }, - documentation: makeDocString( - '你可以在任意语句后加上参数 -next,这样做可以在执行完本条语句后立刻跳转到下一条语句。这对需要在同一时间内执行多步操作非常有用。', - 'changeBg:testBG03.jpg -next; // 会立刻执行下一条语句', - ), - }), - ); - result.push( - setKind({ - label: 'when?', - insertText: makeInsertText('when', insertDash, true), - labelDetails: { - description: '根据条件判断当前语句是否要执行', - }, - documentation: makeDocString( - `在语句后加上 -when=(condition) 参数,可以根据条件判断当前语句是否要执行。 -任何语句都可以加上 -when 参数来控制是否执行。通过组合 -when 参数和 jumpLabel callScene changeScene,你可以实现带条件判断的流程控制。`, - `setVar:a=1; - ; // 当 a 大于 1 时跳转到场景 1 - changeScene:1.txt -when=a>1; - ; // 只有 a 为 1 时才跳转,注意相等运算符是 == - changeScene:2.txt -when=a==1; - ; // 如果 a 小于 1,那么上面的语句不执行,自然就执行这一句了 - changeScene:3.txt;`, - ), - }), - ); - - return result; -} - -function isTriggeringByDash(params: CompletionParams) { - return ( - params.context.triggerKind === CompletionTriggerKind.TriggerCharacter && - params.context.triggerCharacter === '-' - ); -} - -function containsDash(lineBefore: string) { - const asciiRemoved = lineBefore.replace(/[A-Za-z]/g, ''); - console.debug(`asciiRemoved: ${asciiRemoved}`); - return asciiRemoved.lastIndexOf('-') === asciiRemoved.length - 1; -} - -export function shouldInsertDash(line: string, params: CompletionParams) { - return ( - !isTriggeringByDash(params) && - !containsDash(line.substring(0, params.position.character)) - ); +export function markdown(content: string): MarkupContent { + return { + kind: MarkupKind.Markdown, + value: content, + }; } diff --git a/packages/terre2/src/Modules/lsp/completion/fileSuggestion.ts b/packages/terre2/src/Modules/lsp/completion/fileSuggestion.ts index c1ef2c402..69571d89a 100644 --- a/packages/terre2/src/Modules/lsp/completion/fileSuggestion.ts +++ b/packages/terre2/src/Modules/lsp/completion/fileSuggestion.ts @@ -2,29 +2,38 @@ import { CompletionItem, CompletionItemKind, CompletionParams, + MarkupContent, + MarkupKind, } from 'vscode-languageserver'; import { ISentence } from 'webgal-parser/build/types/interface/sceneInterface'; import { commandType } from './commandArgs'; import { IFileInfo, WebgalFsService } from '../../webgal-fs/webgal-fs.service'; import { ConsoleLogger } from '@nestjs/common'; -function setKind(item: CompletionItem) { - return { ...item, kind: CompletionItemKind.File }; +function setKind(item: CompletionItem, isDir: boolean) { + return { + ...item, + kind: isDir ? CompletionItemKind.Folder : CompletionItemKind.File, + }; } -export function makeFileSuggestion(files: string[]): CompletionItem[] { +export function makeFileSuggestion(files: IFileInfo[]): CompletionItem[] { return files.map((f) => - setKind({ - label: f, - insertText: f + ' ', - }), + setKind( + { + label: f.name, + insertText: f.name, + }, + f.isDir, + ), ); } const fsService = new WebgalFsService(new ConsoleLogger()); -function getPathFromSubdir(basePath: string, subdir: string) { - return fsService.getPathFromRoot(decodeURI(`public/${basePath}/${subdir}`)); +function getPathFromSubdir(basePath: string, subdir: string, content: string) { + const path = `public/${basePath}/${subdir}/${content}`; + return fsService.getPathFromRoot(decodeURI(path)); } function filterFiles(files: IFileInfo[]) { @@ -32,38 +41,65 @@ function filterFiles(files: IFileInfo[]) { return files.filter((di) => !di.name.startsWith('.')); } +function getPath(line: string): string { + // 匹配第一个冒号到最后一个斜杠之间的内容 + // 包括最后一个斜杠,不包括第一个冒号以及紧接着的一个或多个空格 + const result = line.match('(?<=:\\s*)\\S.*/'); + return result ? result[0] : ''; +} + export async function handleFileSuggestions( sentence: ISentence, basePath: string, + line: string, ): Promise { let dirPath = ''; let dirInfo: IFileInfo[] = []; if (sentence.command === commandType.changeBg) { - dirPath = getPathFromSubdir(basePath, 'background'); + dirPath = getPathFromSubdir(basePath, 'background', getPath(line)); } if (sentence.command === commandType.unlockCg) { - dirPath = getPathFromSubdir(basePath, 'background'); + dirPath = getPathFromSubdir(basePath, 'background', getPath(line)); } if (sentence.command === commandType.changeFigure) { - dirPath = getPathFromSubdir(basePath, 'figure'); + dirPath = getPathFromSubdir(basePath, 'figure', getPath(line)); + } + if (sentence.command === commandType.miniAvatar) { + dirPath = getPathFromSubdir(basePath, 'figure', getPath(line)); } if (sentence.command === commandType.changeScene) { - dirPath = getPathFromSubdir(basePath, 'scene'); + dirPath = getPathFromSubdir(basePath, 'scene', getPath(line)); } if (sentence.command === commandType.callScene) { - dirPath = getPathFromSubdir(basePath, 'scene'); + dirPath = getPathFromSubdir(basePath, 'scene', getPath(line)); } if (sentence.command === commandType.bgm) { - dirPath = getPathFromSubdir(basePath, 'bgm'); + dirPath = getPathFromSubdir(basePath, 'bgm', getPath(line)); } if (sentence.command === commandType.unlockBgm) { - dirPath = getPathFromSubdir(basePath, 'bgm'); + dirPath = getPathFromSubdir(basePath, 'bgm', getPath(line)); } if (sentence.command === commandType.video) { - dirPath = getPathFromSubdir(basePath, 'video'); + dirPath = getPathFromSubdir(basePath, 'video', getPath(line)); + } + if (sentence.command === commandType.playEffect) { + dirPath = getPathFromSubdir(basePath, 'vocal', getPath(line)); + } + if (sentence.command === commandType.choose) { + let path = ''; + // 第一步匹配最后一个冒号后的所有内容 + // 第二步匹配最后一个斜杠前的所有内容 + const result0 = line.match('(?<=:\\s*)[^:|\\|]*$'); + if (result0) { + const result1 = result0[0].match('.*/'); + path = result1 ? result1[0] : ''; + dirPath = getPathFromSubdir(basePath, 'scene', path); + } else { + return []; + } } dirInfo = await fsService.getDirInfo(dirPath); console.debug(dirInfo); - return makeFileSuggestion(filterFiles(dirInfo).map((di) => di.name)); + return makeFileSuggestion(filterFiles(dirInfo)); } diff --git a/packages/terre2/src/Modules/lsp/completion/index.ts b/packages/terre2/src/Modules/lsp/completion/index.ts index 706e1930c..c5b924b98 100644 --- a/packages/terre2/src/Modules/lsp/completion/index.ts +++ b/packages/terre2/src/Modules/lsp/completion/index.ts @@ -9,19 +9,16 @@ import { IScene } from 'webgal-parser/build/types/interface/sceneInterface'; import { pprintJSON } from '../../../util/strings'; import { webgalParser } from '../../../util/webgal-parser'; import { handleFileSuggestions } from './fileSuggestion'; -import { - commandArgs, - commandType, - makeCompletion, - shouldInsertDash, -} from './commandArgs'; +import { commandType } from './commandArgs'; import { lastVariables } from '../webgalLsp'; import { getCommands } from '../suggestionRules/getCommands'; +import { getArgsKey } from '../suggestionRules/getArgsKey'; /** * Cache the last document lines */ let lastDocumentLines = []; +const variableList: Map = new Map(); export function checkTriggerCompletion( params: TextDocumentChangeEvent, @@ -29,6 +26,14 @@ export function checkTriggerCompletion( ) { const currentDocumentLines: string[] = []; let changedLine = -1; + + variableList.clear(); + variableList.set('Game_name', -1); + variableList.set('Game_key', -1); + variableList.set('Title_img', -1); + variableList.set('Title_bgm', -1); + variableList.set('Game_Logo', -1); + for (let i = 0; i < params.document.lineCount; i++) { const line = params.document .getText({ @@ -43,6 +48,16 @@ export function checkTriggerCompletion( lastDocumentLines[i] = line; changedLine = i; } + + const variable = line.match('(?<=setVar:\\s*)\\w*'); + if (variable) { + variableList.set(variable[0], i); + } + + const userInput = line.match('(?<=getUserInput:\\s*)\\w*'); + if (userInput) { + variableList.set(userInput[0], i); + } } if (!lastDocumentLines) { lastDocumentLines = currentDocumentLines; @@ -57,14 +72,14 @@ export function checkTriggerCompletion( } } -function suggestVariables(params: CompletionParams, postfix = '') { +function suggestVariables(params: CompletionParams) { const result = []; - lastVariables.forEach((v, k) => { + variableList.forEach((v, k) => { if (v <= params.position.line) { result.push({ label: k, - insertText: k + postfix, + insertText: k, kind: CompletionItemKind.Variable, }); } @@ -84,18 +99,21 @@ export async function complete( end: position, }); - // Before receving `:`, consider waiting for a new command - // NOTE: this may not be the case if the same character is saying, but we - // don't have other ways to distinguish these two cases - if (!line.includes(':')) { - return getCommands(line); - } - // If cursor after comment region, disable completion if (line.includes(';') && position.character > line.indexOf(';')) { return []; } + if (line.endsWith('{')) { + return suggestVariables(params); + } + + // If there's no ':' and cursor position is at the start of line + // consider waiting for a new command + if (line.match('^\\w*$')) { + return getCommands(); + } + console.debug(`Line to complete: ${line}`); let suggestions: CompletionItem[] = []; @@ -115,28 +133,104 @@ export async function complete( let newSuggestions: CompletionItem[] = []; - if (line.charAt(params.position.character - 1) === ':') { - if (sentence.command === commandType.say) { - // No suggestions for conversation - newSuggestions = []; - } else if (sentence.command === commandType.setVar) { - // Suggest existing variables for value updates - newSuggestions = suggestVariables(params, '='); - } else { - // Encountering file name input. Do file suggestions - newSuggestions = await handleFileSuggestions(sentence, basePath); - } - } else if (line.charAt(params.position.character - 1) === '{') { - if (sentence.command === commandType.say) { - // Suggest variables - newSuggestions = suggestVariables(params, '}'); + if (line.includes(' -')) { + if (line.match('\\s\\-(\\w*?)$')) { + newSuggestions = getArgsKey(line, sentence.command); } } else { - // No file suggestions. Check completion - newSuggestions = makeCompletion( - commandArgs[sentence.command], - shouldInsertDash(line, params), - ); + switch (sentence.command) { + case commandType.say: { + break; + } + case commandType.changeBg: { + newSuggestions = await handleFileSuggestions( + sentence, + basePath, + line, + ); + break; + } + case commandType.changeFigure: { + newSuggestions = await handleFileSuggestions( + sentence, + basePath, + line, + ); + break; + } + case commandType.bgm: { + newSuggestions = await handleFileSuggestions( + sentence, + basePath, + line, + ); + break; + } + case commandType.video: { + newSuggestions = await handleFileSuggestions( + sentence, + basePath, + line, + ); + break; + } + case commandType.playEffect: { + newSuggestions = await handleFileSuggestions( + sentence, + basePath, + line, + ); + break; + } + case commandType.miniAvatar: { + newSuggestions = await handleFileSuggestions( + sentence, + basePath, + line, + ); + break; + } + case commandType.changeScene: { + newSuggestions = await handleFileSuggestions( + sentence, + basePath, + line, + ); + break; + } + case commandType.callScene: { + newSuggestions = await handleFileSuggestions( + sentence, + basePath, + line, + ); + break; + } + case commandType.unlockCg: { + newSuggestions = await handleFileSuggestions( + sentence, + basePath, + line, + ); + break; + } + case commandType.unlockBgm: { + newSuggestions = await handleFileSuggestions( + sentence, + basePath, + line, + ); + break; + } + case commandType.choose: { + newSuggestions = await handleFileSuggestions( + sentence, + basePath, + line, + ); + break; + } + } } suggestions = suggestions.concat(newSuggestions); diff --git a/packages/terre2/src/Modules/lsp/suggestionRules/getArgsKey.ts b/packages/terre2/src/Modules/lsp/suggestionRules/getArgsKey.ts index e6a63d889..1628a2bfa 100644 --- a/packages/terre2/src/Modules/lsp/suggestionRules/getArgsKey.ts +++ b/packages/terre2/src/Modules/lsp/suggestionRules/getArgsKey.ts @@ -1,91 +1,728 @@ -import { - CompletionItem, - CompletionItemKind, - Position, -} from 'vscode-languageserver'; +import { CompletionItem, CompletionItemKind } from 'vscode-languageserver'; +import { commandType, markdown } from '../completion/commandArgs'; export function getArgsKey( line: string, - allTextBefore: string, - position: Position, + command: commandType, ): CompletionItem[] { - if (line.endsWith(' -') && line.startsWith('setAnimation:')) { - return [...abbrKeys, ...keyNames, ...setAnimationKeys]; + switch (command) { + case commandType.say: { + return [ + whenKey, + notendKey, + concatKey, + fontSizeKey, + idFigureKey, + figureIdKey, + speakerKey, + vocalKey, + clearKey, + ]; + } + case commandType.changeBg: { + return [ + whenKey, + nextKey, + durationKey, + transformKey, + unlocknameKey, + seriesKey, + ]; + } + case commandType.choose: { + return [whenKey]; + } + case commandType.getUserInput: { + return [whenKey, nextKey, titleKey, buttonTextKey, defaultValueKey]; + } + case commandType.bgm: { + return [whenKey, volumeKey, enterBgmKey, unlocknameKey, seriesKey]; + } + case commandType.changeFigure: { + return [ + whenKey, + nextKey, + durationKey, + idFigureKey, + leftKey, + rightKey, + transformKey, + zIndexKey, + motionKey, + expressionKey, + boundsKey, + animationFlagKey, + eyesOpenKey, + eyesCloseKey, + mouthOpenKey, + mouthHalfOpenKey, + mouthCloseKey, + ]; + } + case commandType.intro: { + return [ + whenKey, + backgroundColorKey, + fontColorKey, + fontSizeKey, + animationKey, + delayTimeKey, + holdKey, + userForwardKey, + ]; + } + case commandType.playEffect: { + return [whenKey, volumeKey, idSoundKey]; + } + case commandType.video: { + return [whenKey, skipOffKey]; + } + case commandType.setAnimation: { + return [whenKey, nextKey, targetKey]; + } + case commandType.setTransform: { + return [whenKey, nextKey, targetKey, durationKey]; + } + case commandType.setTransition: { + return [whenKey, targetKey, enterAnimationKey, exitAnimationKey]; + } + case commandType.setVar: { + return [whenKey, globalKey]; + } + case commandType.unlockCg: { + return [whenKey, nameKey, seriesKey]; + } + case commandType.unlockBgm: { + return [whenKey, nameKey, seriesKey]; + } + default: { + return [whenKey]; + } } - if (line.endsWith(' -') && line.startsWith('changeFigure:')) { - return [...abbrKeys, ...keyNames, ...figureKeys]; - } - if (line.endsWith(' -') && line.includes(':')) { - return [...abbrKeys, ...keyNames]; - } - return []; } -const abbrKeys: CompletionItem[] = [ - { - label: '-next', - kind: CompletionItemKind.Constant, - documentation: `连续执行本句和下一句`, - detail: `option -next`, - insertText: 'next', - }, - { - label: '-notend', - kind: CompletionItemKind.Constant, - documentation: `用于对话,表示该对话未结束`, - detail: `option -notend`, - insertText: 'notend', - }, - { - label: '-concat', - kind: CompletionItemKind.Constant, - documentation: `用于对话,将该对话与上一句连接`, - detail: `option -concat`, - insertText: 'concat', - }, -]; - -const keyNames: CompletionItem[] = [ - { - label: '-when', - kind: CompletionItemKind.Constant, - documentation: `条件执行本句 -changeScene:2.txt -when=a>1;a>1时跳转到场景2`, - detail: `option -when=`, - insertText: 'when=', - }, -]; - -const figureKeys: CompletionItem[] = [ - { - label: '-left', - kind: CompletionItemKind.Constant, - documentation: `立绘设置在左侧`, - detail: `option -left`, - insertText: 'left', - }, - { - label: '-right', - kind: CompletionItemKind.Constant, - documentation: `立绘设置在右侧`, - detail: `option -right`, - insertText: 'right', - }, - { - label: '-id', - kind: CompletionItemKind.Constant, - documentation: `设置立绘 ID`, - detail: `option -id=`, - insertText: 'id=', - }, -]; - -const setAnimationKeys: CompletionItem[] = [ - { - label: '-target', - kind: CompletionItemKind.Constant, - documentation: `设置动画目标 ID`, - detail: `option -target=`, - insertText: 'target=', - }, -]; +const whenKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'when', + insertText: 'when=', + detail: '条件执行', + documentation: markdown(` +在语句后加上 \`-when=(condition)\` 参数,可以根据条件判断当前语句是否要执行。 + +例如: + +\`\`\` +setVar:a=1; +; // 当 a 大于 1 时跳转到场景 1 +changeScene:1.txt -when=a>1; +; // 只有 a 为 1 时才跳转,注意相等运算符是 == +changeScene:2.txt -when=a==1; +; // 如果 a 小于 1,那么上面的语句不执行,自然就执行这一句了 +changeScene:3.txt; + +\`\`\` + +> \`=\` 是赋值符号,不可用于条件判断,\`==\`是相等运算符。 + + +任何语句都可以加上 \`-when\` 参数来控制是否执行。通过组合 \`-when\` 参数和 \`jumpLabel\` \`callScene\` \`changeScene\`,你可以实现带条件判断的流程控制。 + `), +}; + +const nextKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'next', + insertText: 'next', + detail: '连续执行', + documentation: markdown(` +你可以在任意语句后加上参数 \`-next\`,这样做可以在执行完本条语句后立刻跳转到下一条语句。这对需要在同一时间内执行多步操作非常有用。 + +示例: + +\`\`\` +changeBg:testBG03.jpg -next; // 会立刻执行下一条语句 +\`\`\` + `), +}; + +const durationKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'duration', + insertText: 'duration=', + detail: '持续时间', + documentation: markdown(` +这个时间片的持续时间,单位为毫秒(ms) + `), +}; + +const figureIdKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'figureId', + insertText: 'figureId=', + detail: '指定立绘ID', + documentation: markdown(` +为对话指定立绘ID,可同步该立绘的唇形 + `), +}; + +const fontSizeKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'fontSize', + insertText: 'fontSize=', + detail: '字体大小', + documentation: markdown(` +调整字体大小 + `), +}; + +const notendKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'notend', + insertText: 'notend', + detail: '文字展示完执行下一句', + documentation: markdown(` +有时候,可能你希望在某一句对话执行到某个阶段时加入演出效果,比如切换表情等。 +这时候,你可以使用 \`-notend\` \`-concat\` 参数来实现在对话中插入任意语句。 + +\`-concat\` 代表本句对话连接在上一句对话之后 + +\`-notend\` 代表本句对话没有结束,在后面可能连接演出或对话。 + +示例如下:这是一个在对话进行中切换立绘的演示。 + +\`\`\` +WebGAL:测试语句插演出!马上切换立绘...... -notend; +changeFigure:k1.png -next; +切换立绘!马上切换表情...... -notend -concat; +changeFigure:k2.png -next; +切换表情! -concat; +\`\`\` + +你也可以只使用 \`-concat\` 参数,将下一句连接在上一句对话之后,因为 \`-notend\` 参数会在对话渐显完成后转到下一句。 + +\`\`\` +这是第一句......; +用户点击鼠标后才会转到第二句 -concat; +\`\`\` + `), +}; + +const concatKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'concat', + insertText: 'concat', + detail: '将该对话与上一句连接', + documentation: markdown(` +有时候,可能你希望在某一句对话执行到某个阶段时加入演出效果,比如切换表情等。 +这时候,你可以使用 \`-notend\` \`-concat\` 参数来实现在对话中插入任意语句。 + +\`-concat\` 代表本句对话连接在上一句对话之后 + +\`-notend\` 代表本句对话没有结束,在后面可能连接演出或对话。 + +示例如下:这是一个在对话进行中切换立绘的演示。 + +\`\`\` +WebGAL:测试语句插演出!马上切换立绘...... -notend; +changeFigure:k1.png -next; +切换立绘!马上切换表情...... -notend -concat; +changeFigure:k2.png -next; +切换表情! -concat; +\`\`\` + +你也可以只使用 \`-concat\` 参数,将下一句连接在上一句对话之后,因为 \`-notend\` 参数会在对话渐显完成后转到下一句。 + +\`\`\` +这是第一句......; +用户点击鼠标后才会转到第二句 -concat; +\`\`\` + `), +}; + +const leftKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'left', + insertText: 'left', + detail: '将立绘置于左侧', + documentation: markdown(` +现在,你可以在页面的三个不同位置放置不同的立绘,只需要在放置立绘的语句处加上你要放置的位置就可以了,示例如下: + +\`\`\` +changeFigure:testFigure03.png -left; +changeFigure:testFigure04.png; +changeFigure:testFigure03.png -right; +\`\`\` + +以上三行分别对应着左、中、右三个不同的位置。三个不同位置的立绘是相互独立的,所以如果你需要清除立绘,必须分别独立清除: + +\`\`\` +changeFigure:none -left; +changeFigure:none; +changeFigure:none -right; +\`\`\` + `), +}; + +const rightKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'right', + insertText: 'right', + detail: '将立绘置于右侧', + documentation: markdown(` +现在,你可以在页面的三个不同位置放置不同的立绘,只需要在放置立绘的语句处加上你要放置的位置就可以了,示例如下: + +\`\`\` +changeFigure:testFigure03.png -left; +changeFigure:testFigure04.png; +changeFigure:testFigure03.png -right; +\`\`\` + +以上三行分别对应着左、中、右三个不同的位置。三个不同位置的立绘是相互独立的,所以如果你需要清除立绘,必须分别独立清除: + +\`\`\` +changeFigure:none -left; +changeFigure:none; +changeFigure:none -right; +\`\`\` + `), +}; + +const idFigureKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'id', + insertText: 'id=', + detail: '设置id', + documentation: markdown(` +如果你想要更精确地控制立绘,或使用超过 3 个立绘,可以为立绘指定 \`id\` 和初始位置: + +\`\`\` +; // 一个初始位置在右侧的自由立绘 +changeFigure:testFigure03.png -left -id=test1; +; // 通过 id 关闭立绘 +changeFigure:none -id=test1; +\`\`\` + +> 如果你要重设某个带ID立绘的位置,请先关闭再重新打开。 + `), +}; + +const idSoundKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'id', + insertText: 'id=', + detail: '设置id', + documentation: markdown(` +为效果音赋予一个 \`id\` 将会自动启用效果音循环,后续使用相同的 \`id\` 来停止。 + +\`\`\` +playEffect:xxx.mp3 -id=xxx; +playEffect:none -id=xxx; // 停止这个循环的效果音 +\`\`\` + `), +}; + +const transformKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'transform', + insertText: 'transform=', + detail: '设置变换效果', + documentation: markdown(` +有关效果的字段说明,请参考 [动画](https://docs.openwebgal.com/webgal-script/animation.html) + +你可以在设置立绘或背景的时候就为立绘设置一些变换和滤镜效果,以下是一个示例: + +\`\`\` +changeFigure:stand.png -transform={"alpha":1,"position":{"x":0,"y":500},"scale":{"x":1,"y":1},"rotation":0,"blur":0,"brightness":1,"contrast":1,"saturation":1,"gamma":1,"colorRed":255,"colorGreen":255,"colorBlue":255,"oldFilm":0,"dotFilm":0,"reflectionFilm":0,"glitchFilm":0,"rgbFilm":0,"godrayFilm":0} -next; +\`\`\` + `), +}; + +const zIndexKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'zIndex', + insertText: 'zIndex=', + detail: '图层排序', + documentation: markdown(` +图层排序索引值,值越大越靠上,值相同时晚加入的靠上 + +\`\`\` +changeFigure:xxx.png -id=xxx -zIndex=0; +changeFigure:yyy.png -id=yyy -zIndex=1; +\`\`\` + `), +}; + +const animationFlagKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'animationFlag', + insertText: 'animationFlag=', + detail: '唇形同步与眨眼', + documentation: markdown(` +当 \`animationFlag\` 设置为 \`on\` 时,可为图片立绘开启唇形同步与眨眼 +本质上是多个静态图片切换 + +\`\`\` +changeFigure:char.png -animationFlag=on -eyesOpen=char_eyes_open.png -eyesClose=char_eyes_close.png -mouthOpen=mouth.png -mouthHalfOpen=char_mouth_half_open.png -mouthClose=char_mouth_close.png; +\`\`\` + `), +}; + +const eyesOpenKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'eyesOpen', + insertText: 'eyesOpen=', + detail: '眼睛睁开的图片立绘', + documentation: markdown(` +当 \`animationFlag\` 设置为 \`on\` 时,可为图片立绘开启唇形同步与眨眼 +本质上是多个静态图片切换 + +\`\`\` +changeFigure:char.png -animationFlag=on -eyesOpen=char_eyes_open.png -eyesClose=char_eyes_close.png -mouthOpen=mouth.png -mouthHalfOpen=char_mouth_half_open.png -mouthClose=char_mouth_close.png; +\`\`\` + `), +}; + +const eyesCloseKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'eyesClose', + insertText: 'eyesClose=', + detail: '眼睛闭上的图片立绘', + documentation: markdown(` +当 \`animationFlag\` 设置为 \`on\` 时,可为图片立绘开启唇形同步与眨眼 +本质上是多个静态图片切换 + +\`\`\` +changeFigure:char.png -animationFlag=on -eyesOpen=char_eyes_open.png -eyesClose=char_eyes_close.png -mouthOpen=mouth.png -mouthHalfOpen=char_mouth_half_open.png -mouthClose=char_mouth_close.png; +\`\`\` + `), +}; + +const mouthOpenKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'mouthOpen', + insertText: 'mouthOpen=', + detail: '嘴巴张开的图片立绘', + documentation: markdown(` +当 \`animationFlag\` 设置为 \`on\` 时,可为图片立绘开启唇形同步与眨眼 +本质上是多个静态图片切换 + +\`\`\` +changeFigure:char.png -animationFlag=on -eyesOpen=char_eyes_open.png -eyesClose=char_eyes_close.png -mouthOpen=mouth.png -mouthHalfOpen=char_mouth_half_open.png -mouthClose=char_mouth_close.png; +\`\`\` + `), +}; + +const mouthHalfOpenKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'mouthHalfOpen', + insertText: 'mouthHalfOpen=', + detail: '嘴巴半张开的图片立绘', + documentation: markdown(` +当 \`animationFlag\` 设置为 \`on\` 时,可为图片立绘开启唇形同步与眨眼 +本质上是多个静态图片切换 + +\`\`\` +changeFigure:char.png -animationFlag=on -eyesOpen=char_eyes_open.png -eyesClose=char_eyes_close.png -mouthOpen=mouth.png -mouthHalfOpen=char_mouth_half_open.png -mouthClose=char_mouth_close.png; +\`\`\` + `), +}; + +const mouthCloseKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'mouthClose', + insertText: 'mouthClose=', + detail: '嘴巴闭上的图片立绘', + documentation: markdown(` +当 \`animationFlag\` 设置为 \`on\` 时,可为图片立绘开启唇形同步与眨眼 +本质上是多个静态图片切换 + +\`\`\` +changeFigure:char.png -animationFlag=on -eyesOpen=char_eyes_open.png -eyesClose=char_eyes_close.png -mouthOpen=mouth.png -mouthHalfOpen=char_mouth_half_open.png -mouthClose=char_mouth_close.png; +\`\`\` + `), +}; + +const motionKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'motion', + insertText: 'motion=', + detail: 'live2D的动作', + documentation: markdown(` +你可以使用 \`-motion=motionName\` 或 \`-expression=expressionName\` 参数来切换表情,如: + +\`\`\` +changeFigure:xxx.json -motion=angry -expression=angry01; +\`\`\` + `), +}; + +const expressionKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'expression', + insertText: 'expression=', + detail: 'live2D的表情', + documentation: markdown(` +你可以使用 \`-motion=motionName\` 或 \`-expression=expressionName\` 参数来切换表情,如: + +\`\`\` +changeFigure:xxx.json -motion=angry -expression=angry01; +\`\`\` + `), +}; + +const boundsKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'bounds', + insertText: 'bounds=', + detail: 'live2D的边界', + documentation: markdown(` +当live2D默认显示范围不足时,调整此参数以拓展边界 + +\`\`\` +changeFigure:xxx.json -bounds=0,50,0,50; +\`\`\` + `), +}; + +const unlocknameKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'unlockname', + insertText: 'unlockname=', + detail: '解锁名称', + documentation: markdown(` +CG或音乐解锁进鉴赏模式的命名 + `), +}; + +const seriesKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'series', + insertText: 'series=', + detail: '鉴赏系列名称', + documentation: markdown(` +CG或音乐解锁进鉴赏模式后应当放在哪个系列 + `), +}; + +const targetKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'target', + insertText: 'target=', + detail: '指定目标', + documentation: markdown(` +将动画或效果应用于指定目标 + `), +}; + +const globalKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'global', + insertText: 'global', + detail: '全局变量', + documentation: markdown(` +WebGAL 的普通变量是跟随存档的,也就是说,任何变量只存在于当前的游戏场景中,只有存档才能将其保存下来,读档将其恢复。 + +为了解决可能存在的作者希望设置多周目的问题,提供长效(全局)变量,一旦设置,则在整个游戏中生效,除非用户清除全部数据。 + +加上 \`-global\` 参数可以设置长效(全局)变量 + +\`\`\`ws +setVar:a=1 -global; +\`\`\` + +这样就设置了一个不随存档读取而改变的变量。 + +使用例: + +\`\`\`ws +jumpLabel:turn-2 -when=a>0; +setVar:a=1 -global; +一周目; +changeScene:一周目剧情.txt; +label:turn-2; +二周目; +changeScene:二周目剧情.txt; +\`\`\` + `), +}; + +const nameKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'name', + insertText: 'name=', + detail: '名称', + documentation: markdown(` +指定名称 + `), +}; + +const backgroundColorKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'backgroundColor', + insertText: 'backgroundColor=', + detail: '背景颜色', + documentation: markdown(` +指定背景颜色 + `), +}; + +const fontColorKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'fontColor', + insertText: 'fontColor=', + detail: '字体颜色', + documentation: markdown(` +指定字体颜色 + `), +}; + +const animationKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'animation', + insertText: 'animation=', + detail: '动画', + documentation: markdown(` +指定动画 + `), +}; + +const holdKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'hold', + insertText: 'hold', + detail: '文字显示完不自动播放', + documentation: markdown(` +在手动播放模式下,文字显示完不自动播放下一句 +> 注:此参数对自动播放模式无效 + `), +}; + +const userForwardKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'userForward', + insertText: 'userForward', + detail: '手动播放一行一行文字', + documentation: markdown(` +手动播放一行一行文字 + `), +}; + +const delayTimeKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'delayTime', + insertText: 'delayTime=', + detail: '延迟时长', + documentation: markdown(` +延迟时长 + `), +}; + +const volumeKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'volume', + insertText: 'volume=', + detail: '音量大小', + documentation: markdown(` +设置音量大小 + `), +}; + +const enterBgmKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'enter', + insertText: 'enter=', + detail: '音量淡入时长', + documentation: markdown(` +音量淡入时间 + `), +}; + +const enterAnimationKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'enter', + insertText: 'enter=', + detail: '入场动画', + documentation: markdown(` +设置入场动画 + `), +}; + +const exitAnimationKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'exit', + insertText: 'exit=', + detail: '退场动画', + documentation: markdown(` +设置退场动画 + `), +}; + +const skipOffKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'skipOff', + insertText: 'skipOff', + detail: '禁止跳过', + documentation: markdown(` +禁止跳过 + `), +}; + +const titleKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'title', + insertText: 'title=', + detail: '对话框标题', + documentation: markdown(` +对话框标题 + `), +}; + +const buttonTextKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'buttonText', + insertText: 'buttonText=', + detail: '确认按钮文本', + documentation: markdown(` +确认按钮文本 + `), +}; + +const defaultValueKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'defaultValue', + insertText: 'defaultValue', + detail: '默认值', + documentation: markdown(` +默认值 + `), +}; + +const vocalKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'vocal', + insertText: 'vocal=', + detail: '播放语音文件', + documentation: markdown(` +播放语言文件 + `), +}; + +const speakerKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'speaker', + insertText: 'speaker=', + detail: '说话者', + documentation: markdown(` +说话者 + `), +}; + +const clearKey: CompletionItem = { + kind: CompletionItemKind.Constant, + label: 'clear', + insertText: 'clear', + detail: '清除说话者', + documentation: markdown(` +清除说话者 + `), +}; diff --git a/packages/terre2/src/Modules/lsp/suggestionRules/getCommands.ts b/packages/terre2/src/Modules/lsp/suggestionRules/getCommands.ts index 8eda45a1d..c0180dbc7 100644 --- a/packages/terre2/src/Modules/lsp/suggestionRules/getCommands.ts +++ b/packages/terre2/src/Modules/lsp/suggestionRules/getCommands.ts @@ -1,8 +1,8 @@ import { CompletionItem, CompletionItemKind } from 'vscode-languageserver'; +import { markdown } from '../completion/commandArgs'; -export function getCommands(line: string): CompletionItem[] { - if (!line.split('').includes(':')) return commandSuggestions; - else return []; +export function getCommands(): CompletionItem[] { + return commandSuggestions; } function makeInsertText(text: string) { @@ -11,180 +11,341 @@ function makeInsertText(text: string) { const commandSuggestions: CompletionItem[] = [ { - label: 'intro', kind: CompletionItemKind.Function, - documentation: `黑屏独白 -在许多游戏中,会以黑屏显示一些文字,用来引入主题或表现人物的心理活动。你可以使用 intro 命令来演出独白。 -独白的分拆以分隔符(|)来分割,也就是说,每一个 | 代表一个换行。 -intro:回忆不需要适合的剧本,|反正一说出口,|都成了戏言。;`, - detail: `command intro: [|] ...;`, + label: 'intro', insertText: makeInsertText('intro'), + detail: `黑屏独白`, + documentation: markdown( + `在许多游戏中,会以黑屏显示一些文字,用来引入主题或表现人物的心理活动。你可以使用 intro 命令来演出独白。 +独白的分拆以分隔符(|)来分割,也就是说,每一个 | 代表一个换行。 +\`\`\` +intro:回忆不需要适合的剧本,|反正一说出口,|都成了戏言。; +intro: [|] ...; +\`\`\``, + ), }, { - label: 'changeBg', kind: CompletionItemKind.Function, - documentation: `更新背景图片 -changeBg:testBG03.jpg -next;`, - detail: `command changeBg: [-next];`, + label: 'changeBg', insertText: makeInsertText('changeBg'), + detail: `更新背景图片`, + documentation: markdown( + `\`\`\` +changeBg:testBG03.jpg -next; +changeBg: [-next]; +\`\`\``, + ), }, { - label: 'changeFigure', kind: CompletionItemKind.Function, - documentation: `更新立绘 -changeFigure:testFigure03.png -left -next;`, - detail: `command changeFigure: [-left] [-right] [id=figureId] [-next];`, + label: 'changeFigure', insertText: makeInsertText('changeFigure'), + detail: `更新立绘`, + documentation: markdown( + `\`\`\` +changeFigure:testFigure03.png -left -next; +changeFigure: [-left] [-right] [id=figureId] [-next]; +\`\`\``, + ), }, { - label: 'miniAvatar', kind: CompletionItemKind.Function, - documentation: `放置小头像 -很多游戏可以在文本框的左下角放置小头像,以下是在本引擎中使用的语法 -miniAvatar:minipic_test.png;显示 -miniAvatar:none;关闭`, - detail: `command miniAvatar:;`, + label: 'miniAvatar', insertText: makeInsertText('miniAvatar'), + detail: `放置小头像`, + documentation: markdown( + `很多游戏可以在文本框的左下角放置小头像,以下是在本引擎中使用的语法 +\`\`\` +miniAvatar:minipic_test.png;显示 +miniAvatar:none;关闭 +miniAvatar:; +\`\`\``, + ), }, { - label: 'changeScene', kind: CompletionItemKind.Function, - documentation: `场景跳转 -你可以将你的剧本拆分成多个 txt 文档,并使用一个简单的语句来切换当前运行的剧本。 -changeScene:Chapter-2.txt;`, - detail: `command changeScene:;`, + label: 'changeScene', insertText: makeInsertText('changeScene'), + detail: `场景跳转`, + documentation: markdown( + `你可以将你的剧本拆分成多个 txt 文档,并使用一个简单的语句来切换当前运行的剧本。 +\`\`\` +changeScene:Chapter-2.txt; +changeScene:; +\`\`\``, + ), }, { - label: 'callScene', kind: CompletionItemKind.Function, - documentation: `场景调用 -如果你需要在执行完调用的场景后回到先前的场景(即父场景),你可以使用 callScene 来调用场景 -callScene:Chapter-2.txt;`, - detail: `command callScene:;`, + label: 'callScene', insertText: makeInsertText('callScene'), + detail: `场景调用`, + documentation: markdown( + `如果你需要在执行完调用的场景后回到先前的场景(即父场景),你可以使用 callScene 来调用场景 +\`\`\` +callScene:Chapter-2.txt; +callScene:; +\`\`\``, + ), }, { - label: 'choose', kind: CompletionItemKind.Function, - documentation: `分支选择 -如果你的剧本存在分支选项,你希望通过选择不同的选项进入不同的章节,请使用以下语句。 -其中,|是分隔符。 -choose:叫住她:Chapter-2.txt|回家:Chapter-3.txt;`, - detail: `command choose: [|] ...;`, + label: 'choose', insertText: 'choose: | ;', + detail: `分支选择`, + documentation: markdown( + `如果你的剧本存在分支选项,你希望通过选择不同的选项进入不同的章节,请使用以下语句。 +其中,|是分隔符。 +\`\`\` +choose:叫住她:Chapter-2.txt|回家:Chapter-3.txt; +choose: [|] ...; +\`\`\``, + ), }, { - label: 'end', kind: CompletionItemKind.Function, - documentation: `结束游戏并返回到标题 -end;`, - detail: `command end;`, + label: 'end', insertText: 'end;', + detail: `结束游戏并返回到标题`, + documentation: markdown( + `结束游戏并返回到标题 +\`\`\` +end; +\`\`\``, + ), }, { - label: 'bgm', kind: CompletionItemKind.Function, - documentation: `背景音乐(BGM) -bgm:夏影.mp3;`, - detail: `command bgm:;`, + label: 'bgm', insertText: makeInsertText('bgm'), + detail: `背景音乐(BGM)`, + documentation: markdown( + `\`\`\` +bgm:夏影.mp3; +bgm:; +\`\`\``, + ), }, { - label: 'playEffect', kind: CompletionItemKind.Function, - documentation: `效果音 -playEffect:xxx.mp3;`, - detail: `command playEffect:;`, + label: 'playEffect', insertText: makeInsertText('playEffect'), + detail: `效果音`, + documentation: markdown( + `\`\`\` +playEffect:xxx.mp3; +playEffect:; +\`\`\``, + ), }, { - label: 'playVideo', kind: CompletionItemKind.Function, - documentation: `播放视频 -playVideo:OP.mp4;`, - detail: `command playVideo:;`, + label: 'playVideo', insertText: makeInsertText('playVideo'), + detail: `播放视频`, + documentation: markdown( + `\`\`\` +playVideo:OP.mp4; +playVideo:; +\`\`\``, + ), }, { - label: 'unlockCg', kind: CompletionItemKind.Function, - documentation: `解锁 CG 鉴赏 -unlockCg:xgmain.jpeg -name=星光咖啡馆与死神之蝶 -series=1;`, - detail: `command unlockCg: -name=cgName -series=serisId;`, + label: 'unlockCg', insertText: makeInsertText('unlockCg'), + detail: `解锁 CG 鉴赏`, + documentation: markdown( + `\`\`\` +unlockCg:xgmain.jpeg -name=星光咖啡馆与死神之蝶 -series=1; +unlockCg: -name=cgName -series=serisId; +\`\`\``, + ), }, { - label: 'unlockBgm', kind: CompletionItemKind.Function, - documentation: `解锁 BGM 鉴赏 -unlockBgm:s_Title.mp3 -name=Smiling-Swinging!!;`, - detail: `command unlockBgm: -name=bgmName;`, + label: 'unlockBgm', insertText: makeInsertText('unlockBgm'), + detail: `解锁 BGM 鉴赏`, + documentation: markdown( + `\`\`\` +unlockBgm:s_Title.mp3 -name=Smiling-Swinging!!; +unlockBgm: -name=bgmName; +\`\`\``, + ), }, { - label: 'setTextbox', kind: CompletionItemKind.Function, - documentation: `设置文本框开启/关闭 -setTextbox:hide;关闭文本框 -setTextbox:on;开启文本框,可以是除 hide 以外的任意值。`, - detail: `command setTextbox:[hide] [others];`, + label: 'setTextbox', insertText: makeInsertText('setTextbox'), + detail: `设置文本框开启/关闭`, + documentation: markdown( + `\`\`\` +setTextbox:hide;关闭文本框 +setTextbox:on;开启文本框,可以是除 hide 以外的任意值。 +setTextbox:[hide] [others]; +\`\`\``, + ), }, { - label: 'setAnimation', kind: CompletionItemKind.Function, - documentation: `设置动画 -setAnimation:enter-from-bottom -target=fig-center -next;为中间立绘设置一个从下方进入的动画,并转到下一句。`, - detail: `command setAnimation: -target=targetId;`, + label: 'setAnimation', insertText: makeInsertText('setAnimation'), + detail: `设置动画`, + documentation: markdown( + `\`\`\` +setAnimation:enter-from-bottom -target=fig-center -next;为中间立绘设置一个从下方进入的动画,并转到下一句。 +setAnimation: -target=targetId; +\`\`\``, + ), }, { - label: 'pixiInit', kind: CompletionItemKind.Function, - documentation: `初始化 Pixi 特效 -1.如果你要使用特效,那么你必须先运行这个命令来初始化 Pixi。 -2.如果你想要消除已经作用的效果,你可以使用这个语法来清空效果。`, - detail: `command pixiInit;`, + label: 'pixiInit', insertText: 'pixiInit;', + detail: `初始化 Pixi 特效`, + documentation: markdown( + `1.如果你要使用特效,那么你必须先运行这个命令来初始化 Pixi。 +2.如果你想要消除已经作用的效果,你可以使用这个语法来清空效果。 +\`\`\` +pixiInit; +\`\`\``, + ), }, { - label: 'pixiPerform', kind: CompletionItemKind.Function, - documentation: `初始化 Pixi 特效 -注意:特效作用后,如果没有初始化,特效会一直运行。`, - detail: `command pixiPerform:;`, + label: 'pixiPerform', insertText: makeInsertText('pixiPerform'), + detail: `应用 Pixi 特效`, + documentation: markdown( + `注意:特效作用后,如果没有初始化,特效会一直运行。 +\`\`\` +pixiPerform:; +\`\`\``, + ), }, { - label: 'setVar', kind: CompletionItemKind.Function, - documentation: `使用变量 + label: 'setVar', + insertText: makeInsertText('setVar'), + detail: `设置变量`, + documentation: markdown( + `\`\`\` setVar:a=1;可以设置数字 setVar:a=true;可以设置布尔值 -setVar:a=人物名称;可以设置字符串`, - detail: `command setVar:;`, - insertText: makeInsertText('setVar'), +setVar:a=人物名称;可以设置字符串 +setVar:; +\`\`\``, + ), }, { - label: 'getUserInput', kind: CompletionItemKind.Function, - documentation: `获取用户输入 -getUserInput:name -title=如何称呼你 -buttonText=确认; 将用户输入写入 name 变量中 - `, - detail: `command getUserInput: -title=titleText -buttonText=buttonText;`, + label: 'getUserInput', insertText: makeInsertText('getUserInput'), + detail: `获取用户输入`, + documentation: markdown( + `\`\`\` +getUserInput:name -title=如何称呼你 -buttonText=确认; 将用户输入写入 name 变量中 +getUserInput: -title=titleText -buttonText=buttonText; +\`\`\``, + ), }, { - label: 'setTransition', kind: CompletionItemKind.Function, - documentation: `设置进出场效果 -注意:只有当立绘或背景被设置后,你才能为其设置进出场效果。 + label: 'setTransition', + insertText: makeInsertText('setTransition'), + detail: `设置进出场效果`, + documentation: markdown( + `注意:只有当立绘或背景被设置后,你才能为其设置进出场效果。 设置进出场效果的代码写在立绘或背景的设置代码后。 并且,设置进出场效果的语句必须紧随设置立绘或背景的语句连续执行,否则无法被正确应用。 +\`\`\` setTransition: -target=fig-center -enter=enter-from-bottom -exit=exit; - `, - detail: `command setTransition: -target=targetId -enter=animationName -exit=animationName;`, - insertText: makeInsertText('setTransition'), +setTransition: -target=targetId -enter=animationName -exit=animationName; +\`\`\``, + ), + }, + { + kind: CompletionItemKind.Function, + label: 'setTransform', + insertText: makeInsertText('setTransform'), + detail: `设置变换效果`, + documentation: markdown( + `为已有的立绘或背景设置变换效果 +\`\`\` +setTransform: -target=fig-center -duration=500; +setTransform: -target= -duration=number; +\`\`\``, + ), + }, + { + kind: CompletionItemKind.Function, + label: 'label', + insertText: makeInsertText('label'), + detail: `设置标签`, + documentation: markdown( + `设置标签后,配合 \`jumpLabel\` 或 \`choose\` 可实现语句跳转 +\`\`\` +...... +jumpLabel:label_1; // 跳转到 label_1 +...... +...... +label:label_1; // 创建名为 label_1 的 label +...... +...... +choose:分支 1:part_1|分支 2:part_2; +label:part_1; // 创建名为 part_1 的 label +...... +...... +label:part_2; // 创建名为 part_2 的 label +...... +\`\`\``, + ), + }, + { + kind: CompletionItemKind.Function, + label: 'jumpLabel', + insertText: makeInsertText('jumpLabel'), + detail: `设置标签`, + documentation: markdown( + `设置标签后,配合 \`jumpLabel\` 或 \`choose\` 可实现语句跳转 +\`\`\` +...... +jumpLabel:label_1; // 跳转到 label_1 +...... +...... +label:label_1; // 创建名为 label_1 的 label +...... +...... +choose:分支 1:part_1|分支 2:part_2; +label:part_1; // 创建名为 part_1 的 label +...... +...... +label:part_2; // 创建名为 part_2 的 label +...... +\`\`\``, + ), + }, + { + kind: CompletionItemKind.Function, + label: 'filmMode', + insertText: makeInsertText('filmMode'), + detail: `电影模式`, + documentation: markdown( + `使用 \`filmMode:enable;\` 来开启电影模式。 +使用 \`filmMode:none;\` 来关闭电影模式。`, + ), + }, + { + kind: CompletionItemKind.Function, + label: 'wait', + insertText: makeInsertText('wait'), + detail: `等待一段时间`, + documentation: markdown( + `等待一段时间,单位为毫秒 +\`\`\` +wait: 5000; 等待5秒钟 +\`\`\``, + ), }, ]; diff --git a/packages/terre2/src/Modules/lsp/webgalLsp.ts b/packages/terre2/src/Modules/lsp/webgalLsp.ts index 011dd0efe..191b9ca04 100644 --- a/packages/terre2/src/Modules/lsp/webgalLsp.ts +++ b/packages/terre2/src/Modules/lsp/webgalLsp.ts @@ -46,7 +46,7 @@ export function createWsConnection( // Tell the client that this server supports code completion. completionProvider: { resolveProvider: true, - triggerCharacters: ['-', ':', '{'], + triggerCharacters: ['-', ':', '{', '/'], }, semanticTokensProvider: { full: true, @@ -126,17 +126,19 @@ export function createWsConnection( * request instead of `connection.languages.semanticTokens.on()`. * Reference: https://github.com/microsoft/vscode-discussions/discussions/819 */ - connection.onRequest( - 'textDocument/semanticTokens/full', - (params: SemanticTokensParams): SemanticTokens => { - const result = makeSemanticTokensFullResponse( - params, - documents.get(params.textDocument.uri), - ); - // console.log(`semanticTokens: data: ${result.data}`); - return result; - }, - ); + // 由于 TextEditor.tsx 配置了 setLanguageConfiguration + // 这里似乎不再起作用,因而把部分逻辑搬至其他地方 + // connection.onRequest( + // 'textDocument/semanticTokens/full', + // (params: SemanticTokensParams): SemanticTokens => { + // const result = makeSemanticTokensFullResponse( + // params, + // documents.get(params.textDocument.uri), + // ); + // // console.log(`semanticTokens: data: ${result.data}`); + // return result; + // }, + // ); /*************************** USER CONFIGURATION ****************************/ /**