jbilcke-hf HF staff commited on
Commit
316ef9c
β€’
1 Parent(s): a477577

add text highlight

Browse files
src/components/editor/ScriptEditor/index.tsx CHANGED
@@ -10,11 +10,12 @@ import { useTheme } from "@/controllers/ui/useTheme"
10
  import { themes } from "@/controllers/ui/theme"
11
 
12
  import "./styles.css"
 
13
 
14
  export function ScriptEditor() {
15
 
16
- const editor = useEditor(s => s.editor)
17
- const setEditor = useEditor(s => s.setEditor)
18
  const draft = useEditor(s => s.draft)
19
  const setDraft = useEditor(s => s.setDraft)
20
  const loadDraftFromClap = useEditor(s => s.loadDraftFromClap)
@@ -38,14 +39,14 @@ export function ScriptEditor() {
38
  )
39
 
40
  useEffect(() => {
41
- if (!editor) { return }
42
  // let's do something basic for now: we disable the
43
  // timeline-to-editor scroll sync when the user is
44
  // hovering the editor
45
  if (useEditor.getState().mouseIsInside) { return }
46
 
47
- if (horizontalTimelineRatio !== editor.getScrollTop()) {
48
- editor.setScrollPosition({ scrollTop: horizontalTimelineRatio })
49
  }
50
  // various things we can do here!
51
  // move the scroll:
@@ -63,27 +64,29 @@ export function ScriptEditor() {
63
  // => I think we should restore the "follow cursor during playback"
64
  // feature, because this is doable.
65
 
66
- }, [editor, horizontalTimelineRatio])
67
 
68
- const onMount = (editor: MonacoEditor.editor.IStandaloneCodeEditor) => {
69
- const model = editor.getModel()
70
- if (!model) { return }
71
 
72
- setEditor(editor)
 
 
73
 
74
- editor.onMouseDown((e) => {
75
- jumpCursorOnLineClick(editor.getPosition()?.lineNumber)
76
  })
77
 
78
- editor.onDidScrollChange(({ scrollTop, scrollLeft, scrollWidth, scrollHeight }: MonacoEditor.IScrollEvent) => {
79
  onDidScrollChange({ scrollTop, scrollLeft, scrollWidth, scrollHeight })
80
  })
81
 
82
  // as an optimization we can use this later, for surgical edits,
83
  // to perform real time updates of the timeline
84
 
85
- model.onDidChangeContent((modelContentChangedEvent: MonacoEditor.editor.IModelContentChangedEvent) => {
86
- // console.log("onDidChangeContent:")
87
  for (const change of modelContentChangedEvent.changes) {
88
  // console.log(" - change:", change)
89
  }
@@ -95,6 +98,7 @@ export function ScriptEditor() {
95
  }
96
 
97
  const setMonaco = useEditor(s => s.setMonaco)
 
98
  const setMouseIsInside = useEditor(s => s.setMouseIsInside)
99
  const themeName = useUI(s => s.themeName)
100
  const editorFontSize = useUI(s => s.editorFontSize)
@@ -128,19 +132,23 @@ export function ScriptEditor() {
128
  }
129
 
130
  // Apply the custom theme immediately after defining it
131
- monaco.editor.setTheme(themes.backstage.id)
 
 
 
 
 
 
132
  }
133
 
134
  return (
135
  <div
136
  className="h-full"
137
  onMouseEnter={() => setMouseIsInside(true)}
138
- onMouseLeave={() => setMouseIsInside(false)}
139
  >
140
  <Editor
141
  height="100%"
142
- defaultLanguage="plaintext"
143
- defaultValue={draft}
144
  beforeMount={beforeMount}
145
  theme={themeName}
146
  onMount={onMount}
 
10
  import { themes } from "@/controllers/ui/theme"
11
 
12
  import "./styles.css"
13
+ import { ClapSegmentCategory } from "@aitube/clap"
14
 
15
  export function ScriptEditor() {
16
 
17
+ const standaloneCodeEditor = useEditor(s => s.standaloneCodeEditor)
18
+ const setStandaloneCodeEditor = useEditor(s => s.setStandaloneCodeEditor)
19
  const draft = useEditor(s => s.draft)
20
  const setDraft = useEditor(s => s.setDraft)
21
  const loadDraftFromClap = useEditor(s => s.loadDraftFromClap)
 
39
  )
40
 
41
  useEffect(() => {
42
+ if (!standaloneCodeEditor) { return }
43
  // let's do something basic for now: we disable the
44
  // timeline-to-editor scroll sync when the user is
45
  // hovering the editor
46
  if (useEditor.getState().mouseIsInside) { return }
47
 
48
+ if (horizontalTimelineRatio !== standaloneCodeEditor.getScrollTop()) {
49
+ standaloneCodeEditor.setScrollPosition({ scrollTop: horizontalTimelineRatio })
50
  }
51
  // various things we can do here!
52
  // move the scroll:
 
64
  // => I think we should restore the "follow cursor during playback"
65
  // feature, because this is doable.
66
 
67
+ }, [standaloneCodeEditor, horizontalTimelineRatio])
68
 
69
+ const onMount = (codeEditor: MonacoEditor.editor.IStandaloneCodeEditor) => {
70
+ const { textModel } = useEditor.getState()
71
+ if (!textModel) { return }
72
 
73
+ codeEditor.setModel(textModel)
74
+
75
+ setStandaloneCodeEditor(codeEditor)
76
 
77
+ codeEditor.onMouseDown((e) => {
78
+ jumpCursorOnLineClick(codeEditor.getPosition()?.lineNumber)
79
  })
80
 
81
+ codeEditor.onDidScrollChange(({ scrollTop, scrollLeft, scrollWidth, scrollHeight }: MonacoEditor.IScrollEvent) => {
82
  onDidScrollChange({ scrollTop, scrollLeft, scrollWidth, scrollHeight })
83
  })
84
 
85
  // as an optimization we can use this later, for surgical edits,
86
  // to perform real time updates of the timeline
87
 
88
+ textModel.onDidChangeContent((modelContentChangedEvent: MonacoEditor.editor.IModelContentChangedEvent) => {
89
+ console.log("onDidChangeContent:")
90
  for (const change of modelContentChangedEvent.changes) {
91
  // console.log(" - change:", change)
92
  }
 
98
  }
99
 
100
  const setMonaco = useEditor(s => s.setMonaco)
101
+ const setTextModel = useEditor(s => s.setTextModel)
102
  const setMouseIsInside = useEditor(s => s.setMouseIsInside)
103
  const themeName = useUI(s => s.themeName)
104
  const editorFontSize = useUI(s => s.editorFontSize)
 
132
  }
133
 
134
  // Apply the custom theme immediately after defining it
135
+ monaco.editor.setTheme(themes.backstage.id)
136
+
137
+ const textModel: MonacoEditor.editor.ITextModel = monaco.editor.createModel(
138
+ draft,
139
+ "plaintext"
140
+ )
141
+ setTextModel(textModel)
142
  }
143
 
144
  return (
145
  <div
146
  className="h-full"
147
  onMouseEnter={() => setMouseIsInside(true)}
148
+ onMouseLeave={() => setMouseIsInside(false)}
149
  >
150
  <Editor
151
  height="100%"
 
 
152
  beforeMount={beforeMount}
153
  theme={themeName}
154
  onMount={onMount}
src/components/editor/ScriptEditor/styles.css CHANGED
@@ -1,3 +1,23 @@
1
  .monaco-editor-background {
2
  @apply transition-colors;
3
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  .monaco-editor-background {
2
  @apply transition-colors;
3
+ }
4
+
5
+ .entity {
6
+
7
+ /*
8
+ padding and rounded corners only make sense if we use a background:
9
+ @apply rounded-xl px-1.5 py-1 font-semibold;
10
+ */
11
+ }
12
+
13
+ .entity.entity-location {
14
+ @apply text-green-300/90 !important;
15
+ }
16
+
17
+ .entity.entity-character {
18
+ @apply text-orange-300/90 !important;
19
+ }
20
+
21
+ .entity.entity-highlight {
22
+ @apply font-semibold;
23
+ }
src/controllers/editor/getDefaultEditorState.ts CHANGED
@@ -3,7 +3,8 @@ import { EditorState } from "./types"
3
  export function getDefaultEditorState(): EditorState {
4
  const state: EditorState = {
5
  monaco: undefined,
6
- editor: undefined,
 
7
  mouseIsInside: false,
8
  draft: "",
9
  lineNumberToMentionedSegments: {},
 
3
  export function getDefaultEditorState(): EditorState {
4
  const state: EditorState = {
5
  monaco: undefined,
6
+ textModel: undefined,
7
+ standaloneCodeEditor: undefined,
8
  mouseIsInside: false,
9
  draft: "",
10
  lineNumberToMentionedSegments: {},
src/controllers/editor/types.ts CHANGED
@@ -12,8 +12,10 @@ export type ScrollData = {
12
  export type EditorState = {
13
  monaco?: Monaco
14
 
 
 
15
  // reference to the React component
16
- editor?: MonacoEditor.editor.IStandaloneCodeEditor
17
 
18
  // used to know if the user is actually inside the editor or not
19
  mouseIsInside: boolean
@@ -36,13 +38,16 @@ export type EditorState = {
36
 
37
  export type EditorControls = {
38
  setMonaco: (monaco?: Monaco) => void
39
- setEditor: (editor?: MonacoEditor.editor.IStandaloneCodeEditor) => void
 
40
  setMouseIsInside: (mouseIsInside: boolean) => void
41
  loadDraftFromClap: (clap: ClapProject) => void
42
  setDraft: (draft: string) => void
43
  publishDraftToTimeline: () => Promise<void>
44
  onDidScrollChange: (scrollData: ScrollData) => void
45
  jumpCursorOnLineClick: (line?: number) => void
 
 
46
  }
47
 
48
  export type EditorStore = EditorState & EditorControls
 
12
  export type EditorState = {
13
  monaco?: Monaco
14
 
15
+ textModel?: MonacoEditor.editor.ITextModel
16
+
17
  // reference to the React component
18
+ standaloneCodeEditor?: MonacoEditor.editor.IStandaloneCodeEditor
19
 
20
  // used to know if the user is actually inside the editor or not
21
  mouseIsInside: boolean
 
38
 
39
  export type EditorControls = {
40
  setMonaco: (monaco?: Monaco) => void
41
+ setTextModel: (textModel?: MonacoEditor.editor.ITextModel) => void
42
+ setStandaloneCodeEditor: (standaloneCodeEditor?: MonacoEditor.editor.IStandaloneCodeEditor) => void
43
  setMouseIsInside: (mouseIsInside: boolean) => void
44
  loadDraftFromClap: (clap: ClapProject) => void
45
  setDraft: (draft: string) => void
46
  publishDraftToTimeline: () => Promise<void>
47
  onDidScrollChange: (scrollData: ScrollData) => void
48
  jumpCursorOnLineClick: (line?: number) => void
49
+ highlightElements: () => void
50
+ applyClassNameToKeywords: (className?: string, keywords?: string[], caseSensitive?: boolean) => void
51
  }
52
 
53
  export type EditorStore = EditorState & EditorControls
src/controllers/editor/useEditor.ts CHANGED
@@ -11,8 +11,9 @@ import { Monaco } from "@monaco-editor/react"
11
 
12
  export const useEditor = create<EditorStore>((set, get) => ({
13
  ...getDefaultEditorState(),
14
- setMonaco: (monaco?: Monaco) => { set({ monaco}) },
15
- setEditor: (editor?: MonacoEditor.editor.IStandaloneCodeEditor) => { set({ editor }) },
 
16
  setMouseIsInside: (mouseIsInside: boolean) => { set({ mouseIsInside }) },
17
  loadDraftFromClap: (clap: ClapProject) => {
18
  const { setDraft } = get()
@@ -20,13 +21,17 @@ export const useEditor = create<EditorStore>((set, get) => ({
20
  setDraft(clap.meta.screenplay)
21
  },
22
  setDraft: (draft: string) => {
23
- const { draft: previousDraft } = get()
24
  if (draft === previousDraft) { return }
25
  set({ draft })
26
 
27
- const { editor } = get()
28
- if (!editor) { return }
29
- editor?.setValue(draft)
 
 
 
 
30
  },
31
  publishDraftToTimeline: async (): Promise<void> => {
32
  const { draft } = get()
@@ -72,7 +77,7 @@ export const useEditor = create<EditorStore>((set, get) => ({
72
  const timeline: TimelineStore = useTimeline.getState()
73
  if (!timeline.timelineCamera || !timeline.timelineControls) { return }
74
 
75
- const { editor } = get()
76
 
77
  const scrollRatio = scrollTop / scrollHeight
78
  const scrollX = Math.round(leftBarTrackScaleWidth + scrollRatio * timeline.contentWidth)
@@ -112,6 +117,92 @@ export const useEditor = create<EditorStore>((set, get) => ({
112
  timeline.setCursorTimestampAtInMs(startTimeInMs)
113
  },
114
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  }))
116
 
117
 
 
11
 
12
  export const useEditor = create<EditorStore>((set, get) => ({
13
  ...getDefaultEditorState(),
14
+ setMonaco: (monaco?: Monaco) => { set({ monaco }) },
15
+ setTextModel: (textModel?: MonacoEditor.editor.ITextModel) => { set({ textModel }) },
16
+ setStandaloneCodeEditor: (standaloneCodeEditor?: MonacoEditor.editor.IStandaloneCodeEditor) => { set({ standaloneCodeEditor }) },
17
  setMouseIsInside: (mouseIsInside: boolean) => { set({ mouseIsInside }) },
18
  loadDraftFromClap: (clap: ClapProject) => {
19
  const { setDraft } = get()
 
21
  setDraft(clap.meta.screenplay)
22
  },
23
  setDraft: (draft: string) => {
24
+ const { draft: previousDraft, highlightElements, textModel } = get()
25
  if (draft === previousDraft) { return }
26
  set({ draft })
27
 
28
+
29
+ if (!textModel) { return }
30
+ // we need to update the model
31
+ textModel?.setValue(draft)
32
+
33
+ // and highlight the text again
34
+ highlightElements()
35
  },
36
  publishDraftToTimeline: async (): Promise<void> => {
37
  const { draft } = get()
 
77
  const timeline: TimelineStore = useTimeline.getState()
78
  if (!timeline.timelineCamera || !timeline.timelineControls) { return }
79
 
80
+ const { standaloneCodeEditor } = get()
81
 
82
  const scrollRatio = scrollTop / scrollHeight
83
  const scrollX = Math.round(leftBarTrackScaleWidth + scrollRatio * timeline.contentWidth)
 
117
  timeline.setCursorTimestampAtInMs(startTimeInMs)
118
  },
119
 
120
+ highlightElements: () => {
121
+ const timeline: TimelineStore = useTimeline.getState()
122
+ const { clap } = timeline
123
+
124
+ const { textModel, standaloneCodeEditor, applyClassNameToKeywords } = get()
125
+ if (!textModel || !standaloneCodeEditor || !clap) { return }
126
+
127
+ const characters = clap.entities.filter(entity => entity.category === ClapSegmentCategory.CHARACTER).map(entity => entity.triggerName)
128
+
129
+ // any character
130
+ applyClassNameToKeywords(
131
+ "entity entity-character",
132
+ characters
133
+ )
134
+
135
+ // UPPERCASE CHARACTER
136
+ applyClassNameToKeywords(
137
+ "entity entity-character entity-highlight",
138
+ characters,
139
+ true
140
+ )
141
+
142
+ const locations = clap.entities.filter(entity => entity.category === ClapSegmentCategory.LOCATION).map(entity => entity.triggerName)
143
+ // any location
144
+ applyClassNameToKeywords(
145
+ "entity entity-location",
146
+ locations
147
+ )
148
+
149
+ // UPPERCASE LOCATION
150
+ applyClassNameToKeywords(
151
+ "entity entity-location entity-highlight",
152
+ locations,
153
+ true
154
+ )
155
+ },
156
+ applyClassNameToKeywords: (className: string = "", keywords: string[] = [], caseSensitive = false) => {
157
+ const timeline: TimelineStore = useTimeline.getState()
158
+ const { clap } = timeline
159
+
160
+ const { textModel, standaloneCodeEditor } = get()
161
+ if (!textModel || !standaloneCodeEditor || !clap) { return }
162
+
163
+ keywords.forEach((entityTriggerName: string): void => {
164
+ const matches: MonacoEditor.editor.FindMatch[] = textModel.findMatches(
165
+ // searchString β€” The string used to search. If it is a regular expression, set isRegex to true.
166
+ // searchString: string,
167
+ entityTriggerName,
168
+
169
+ // @param searchOnlyEditableRange β€” Limit the searching to only search inside the editable range of the model.
170
+ // searchOnlyEditableRange: boolean,
171
+ false,
172
+
173
+ // / @param isRegex β€” Used to indicate that searchString is a regular expression.
174
+ // isRegex: boolean,
175
+ false,
176
+
177
+ // @param matchCase β€” Force the matching to match lower/upper case exactly.
178
+ // matchCase: boolean,
179
+ caseSensitive,
180
+
181
+ // @param wordSeparators β€” Force the matching to match entire words only. Pass null otherwise.
182
+ // wordSeparators: string | null,
183
+ null,
184
+
185
+ // @param captureMatches β€” The result will contain the captured groups.
186
+ // captureMatches: boolean,
187
+ false,
188
+
189
+ // limitResultCount β€” Limit the number of results
190
+ // limitResultCount?: number
191
+ )
192
+
193
+ matches.forEach((match: MonacoEditor.editor.FindMatch): void => {
194
+ standaloneCodeEditor.createDecorationsCollection([
195
+ {
196
+ range: match.range,
197
+ options: {
198
+ isWholeLine: false,
199
+ inlineClassName: className
200
+ }
201
+ },
202
+ ])
203
+ })
204
+ })
205
+ }
206
  }))
207
 
208
 
src/controllers/ui/theme.ts CHANGED
@@ -150,12 +150,12 @@ export const lore: UITheme = {
150
  author: "Clapper",
151
  description: "",
152
  defaultBgColor: "#151520",
153
- defaultTextColor: "#E3747B",
154
  defaultPrimaryColor: "#DE4A80",
155
  logoColor: "#DE4A80",
156
  editorBgColor: "#151520",
157
  editorCursorColor: '#DE4A80',
158
- editorTextColor: "#E3747B",
159
  monitorBgColor: "#151520",
160
  monitorSecondaryTextColor: "#D6D3D1",
161
  monitorPrimaryTextColor: "#DE4A80",
 
150
  author: "Clapper",
151
  description: "",
152
  defaultBgColor: "#151520",
153
+ defaultTextColor: "#f6d6d8",
154
  defaultPrimaryColor: "#DE4A80",
155
  logoColor: "#DE4A80",
156
  editorBgColor: "#151520",
157
  editorCursorColor: '#DE4A80',
158
+ editorTextColor: "#f6d6d8",
159
  monitorBgColor: "#151520",
160
  monitorSecondaryTextColor: "#D6D3D1",
161
  monitorPrimaryTextColor: "#DE4A80",