11import {
2+ ChangeDesc ,
23 EditorState ,
34 StateEffect ,
45 StateField ,
@@ -9,7 +10,12 @@ import { Hunks, type Hunk } from "../signs/hunks";
910import { computeHunks } from "./diff" ;
1011import type { Chunk } from "@codemirror/merge" ;
1112import { pluginRef } from "src/pluginGlobalRef" ;
12- import { editorInfoField } from "obsidian" ;
13+ import {
14+ debounce ,
15+ editorEditorField ,
16+ editorInfoField ,
17+ type Debouncer ,
18+ } from "obsidian" ;
1319
1420/**
1521 * Given a document and a position, return the corresponding line number in the
@@ -27,7 +33,7 @@ export function lineFromPos(doc: Text, pos: number): number {
2733export abstract class HunksStateHelper {
2834 static hasHunksData ( state : EditorState ) : boolean {
2935 const data = state . field ( hunksState , false ) ;
30- return ! ! data ;
36+ return ! ! data && ! data . isDirty ;
3137 }
3238
3339 static getHunks ( state : EditorState , staged : boolean ) : Hunk [ ] {
@@ -142,70 +148,205 @@ export const hunksState: StateField<HunksData | undefined> = StateField.define<
142148> ( {
143149 create : ( _state ) => undefined ,
144150 update : ( previous , transaction ) => {
145- const prev : HunksData = previous
151+ const hunksData : HunksData = previous
146152 ? { ...previous }
147153 : {
154+ maxDiffTimeMs : 0 ,
148155 hunks : [ ] ,
149156 stagedHunks : [ ] ,
150157 chunks : undefined ,
158+ isDirty : false ,
151159 } ;
152160 let newCompare = false ;
153161
154162 for ( const effect of transaction . effects ) {
155163 if ( effect . is ( GitCompareResultEffectType ) ) {
156- newCompare = true ;
157- prev . compareText = effect . value . compareText ;
158- prev . compareTextHead = effect . value . compareTextHead ;
164+ hunksData . compareText = effect . value . compareText ;
165+ hunksData . compareTextHead = effect . value . compareTextHead ;
166+
167+ // Only issue new hunk computation if compareText has changed
168+ newCompare = previous ?. compareText !== effect . value . compareText ;
169+ if ( newCompare ) {
170+ hunksData . chunks = undefined ;
171+ }
159172 }
160- }
161- if ( prev . compareText !== undefined ) {
162- const editorText = transaction . state . doc . toString ( ) ;
163- if ( newCompare ) {
164- prev . chunks = undefined ;
173+ if ( effect . is ( DebouncedComputeHunksEffectType ) ) {
174+ applyHunkComputation (
175+ hunksData ,
176+ effect . value ,
177+ transaction . state
178+ ) ;
165179 }
180+ }
181+ if ( hunksData . compareText !== undefined ) {
166182 if ( newCompare || transaction . docChanged ) {
167- const { hunks, chunks } = computeHunks (
168- prev . compareText ,
169- editorText ,
170- prev . chunks ,
171- transaction . changes
172- ) ;
173- // const headHunks = computeHunks(
174- // prev.compareTextHead ?? "",
175- // editorText
176- // );
177- prev . hunks = hunks ;
178- prev . chunks = chunks ;
179- // prev.stagedHunks = Hunks.computeStagedHunks(
180- // headHunks,
181- // hunks,
182- // prev
183- // );
184-
185- const file = transaction . state . field ( editorInfoField ) . file ;
186- pluginRef . plugin ?. editorIntegration . signsFeature . changeStatusBar ?. display (
187- hunks ,
188- file
183+ hunksData . isDirty = true ;
184+ const res = scheduleHunkComputation (
185+ transaction ,
186+ hunksData . compareText ,
187+ hunksData . chunks ,
188+ hunksData . maxDiffTimeMs
189189 ) ;
190+ if ( res ) {
191+ applyHunkComputation ( hunksData , res , transaction . state ) ;
192+ }
190193 }
191194 } else {
192- prev . compareText = undefined ;
193- prev . compareTextHead = undefined ;
194- prev . chunks = undefined ;
195- prev . hunks = [ ] ;
196- prev . stagedHunks = [ ] ;
195+ hunksData . compareText = undefined ;
196+ hunksData . compareTextHead = undefined ;
197+ hunksData . chunks = undefined ;
198+ hunksData . hunks = [ ] ;
199+ hunksData . stagedHunks = [ ] ;
200+ hunksData . isDirty = false ;
197201 }
198- return prev ;
202+ return hunksData ;
199203 } ,
200204} ) ;
201205
206+ function applyHunkComputation (
207+ hunkData : HunksData ,
208+ computeData : ComputedHunksData ,
209+ state : EditorState
210+ ) {
211+ hunkData . hunks = computeData . hunks ;
212+ hunkData . chunks = computeData . chunks ;
213+ hunkData . isDirty = false ;
214+ hunkData . maxDiffTimeMs = Math . max (
215+ 0.95 * hunkData . maxDiffTimeMs ,
216+ computeData . diffDuration
217+ ) ;
218+ const file = state . field ( editorInfoField ) . file ;
219+ pluginRef . plugin ?. editorIntegration . signsFeature . changeStatusBar ?. display (
220+ hunkData . hunks ,
221+ file
222+ ) ;
223+ }
224+
225+ export const computeHunksDebouncerStateField = StateField . define < {
226+ changeDesc ?: ChangeDesc ;
227+ debouncer : Debouncer <
228+ [
229+ {
230+ state : EditorState ;
231+ compareText : string ;
232+ previousChunks : readonly Chunk [ ] | undefined ;
233+ changeDesc : ChangeDesc | undefined ;
234+ } ,
235+ ] ,
236+ void
237+ > ;
238+ } > ( {
239+ create : ( ) => {
240+ return {
241+ debouncer : debounce (
242+ ( data ) => {
243+ const { state, compareText, previousChunks, changeDesc } =
244+ data ;
245+ const res = computeHunksTimed (
246+ state ,
247+ compareText ,
248+ previousChunks ,
249+ changeDesc
250+ ) ;
251+ state . field ( editorEditorField ) . dispatch ( {
252+ effects : DebouncedComputeHunksEffectType . of ( res ) ,
253+ } ) ;
254+ } ,
255+ 1000 ,
256+ true
257+ ) ,
258+ maxDiffTimeMs : 0 ,
259+ } ;
260+ } ,
261+ update : ( data , transaction ) => {
262+ for ( const effect of transaction . effects ) {
263+ if ( effect . is ( DebouncedComputeHunksEffectType ) ) {
264+ data . changeDesc = undefined ;
265+ return data ;
266+ }
267+ }
268+ if ( ! data . changeDesc && transaction . changes ) {
269+ data . changeDesc = transaction . changes ;
270+ } else {
271+ data . changeDesc = data . changeDesc ?. composeDesc ( transaction . changes ) ;
272+ }
273+ return data ;
274+ } ,
275+ } ) ;
276+
277+ function computeHunksTimed (
278+ state : EditorState ,
279+ compareText : string ,
280+ previousChunks : readonly Chunk [ ] | undefined ,
281+ changeDesc : ChangeDesc | undefined
282+ ) : ComputedHunksData {
283+ const editorText = state . doc . toString ( ) ;
284+
285+ const startTime = performance . now ( ) ;
286+ const { hunks, chunks } = computeHunks (
287+ compareText ,
288+ editorText ,
289+ previousChunks ,
290+ changeDesc
291+ ) ;
292+ const diffDuration = performance . now ( ) - startTime ;
293+ return { hunks, chunks, diffDuration } ;
294+ }
295+
296+ function scheduleHunkComputation (
297+ transaction : Transaction ,
298+ compareText : string ,
299+ previousChunks : readonly Chunk [ ] | undefined ,
300+ maxDiffTimeMs : number
301+ ) : ComputedHunksData | undefined {
302+ const state = transaction . state ;
303+ const changeLength = Math . abs (
304+ transaction . changes . length - transaction . changes . newLength
305+ ) ;
306+
307+ const debouncerField = state . field ( computeHunksDebouncerStateField ) ;
308+
309+ // Debounce large changes or if a previous diff took long time
310+ if ( changeLength > 1000 || maxDiffTimeMs > 10 ) {
311+ debouncerField . debouncer ( {
312+ state,
313+ compareText,
314+ previousChunks,
315+ changeDesc : debouncerField . changeDesc ,
316+ } ) ;
317+ } else {
318+ // This technically breaks the immutability of the StateField, but I
319+ // think it's acceptable here. The debouncer itself is not very
320+ // immutable either way.
321+ debouncerField . changeDesc = undefined ;
322+
323+ return computeHunksTimed (
324+ state ,
325+ compareText ,
326+ previousChunks ,
327+ transaction . changes
328+ ) ;
329+ }
330+ }
331+
202332export const GitCompareResultEffectType =
203333 StateEffect . define < GitCompareResult > ( ) ;
204334
335+ export const DebouncedComputeHunksEffectType =
336+ StateEffect . define < ComputedHunksData > ( ) ;
337+
338+ export type ComputedHunksData = {
339+ hunks : Hunk [ ] ;
340+ chunks : readonly Chunk [ ] | undefined ;
341+ diffDuration : number ;
342+ } ;
343+
205344export type HunksData = {
206345 hunks : Hunk [ ] ;
207346 stagedHunks : Hunk [ ] ;
208347 chunks : readonly Chunk [ ] | undefined ;
348+ isDirty : boolean ;
349+ maxDiffTimeMs : number ;
209350} & GitCompareResult ;
210351
211352export type GitCompareResult = {
0 commit comments