(:~ handle textDocument @see https://codemirror.net/docs/ref/#state.Text @author andy bunce :) module namespace doci = 'urn:quodatum:text:doci'; (: default line seperator if none found in text :) declare variable $doci:default-separator:=file:line-separator(); declare record doci:doci( lines as xs:string+, separator? as xs:string ); declare record doci:line( from as xs:integer, (:number The position of the start of the line. :) to as xs:integer, (:The position at the end of the line (before the line break, or at the end of document for the last line).:) number as xs:integer, (: This line's line number (1-based).:) text as xs:string (: The line's content. :) ); (:~ json numbers :) declare type doci:num as (xs:integer|xs:double); (:~ @param line Line position in a document (zero-based). @param character Character offset on a line in a document (zero-based). :) declare record doci:Position( line as doci:num, character as doci:num ); (:~ @param line Line position in a document (zero-based). @param character Character offset on a line in a document (zero-based). :) declare record doci:Range( start as doci:Position, end as doci:Position ); (: @see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didChange :) declare record doci:TextDocumentContentChangeEvent( text as xs:string, range? as doci:Range ); (: create new doci from string :) declare function doci:build($text as xs:string) as doci:doci{ let $ls:=doci:separator($text ) let $lines:= doci:split-lines($text) return doci:doci( lines:= $lines, separator:=$ls ) }; (: return full text :) declare function doci:text($doci as doci:doci) as xs:string{ string-join($doci?lines,$doci?separator) }; (: number of lines:) declare function doci:lines($doci as doci:doci) as xs:integer{ $doci?lines=>count() }; (: detect line separator, assumes all the same:) declare function doci:separator($text as xs:string) as xs:string?{ switch () { case contains($text," ") return " " case contains($text," ") return " " case contains($text," ") return " " default return $doci:default-separator } }; (: apply change:) declare function doci:update($doci as doci:doci,$change as doci:TextDocumentContentChangeEvent) as doci:doci{ let $lines:= doci:split-lines($change?text) return if(empty($change?range)) then doci:doci($lines,$doci?separator) else let $range:=$change?range let $sline:= $range?start?line let $eline:= $range?end?line (: string from start line before insert :) let $head:= substring( $doci?lines[$sline+1] ,1, $range?start?character ) =>trace("$head") (: string from edit end to end of line :) let $last := substring( $doci?lines[$eline+1] , $range?end?character +1) =>trace("$last") let $ulines := ( subsequence($doci?lines, 1, $sline) (: lines before edit :) =>trace("before") , if($sline eq $eline) then ($head || $lines[1] || $last) else if(1=count($lines)) then $head || $lines[1] || $last else ( $head || $lines[1], subsequence( $lines,2, count( $lines - 2)), $lines[last()] ||$last ), subsequence($doci?lines, $eline+2) (: lines after edit :) =>trace("after") ) return doci:doci($ulines,$doci?separator) }; declare function doci:split-lines($text as xs:string) as xs:string+{ if($text eq "") then $text else tokenize($text, '(\r\n?|\n\r?)') }; (: line from pos :) declare function doci:lineAt($doci as doci:doci,$pos as xs:integer) as doci:line { let $starts:=hof:scan-left($doci?lines, 0, fn($res,$line){$res+string-length($doci?seperator)+string-length($line)} ) let $line:=if($pos gt $doci?length) then error(#doci:range,"pos beyond range") else do-until( {"min":1,"max":count($doci?lines)}, fn($r){ let $mid:=round(($r?min+$r?max) div 2,0,"away-from-zero") return if ($doci?starts[$mid] lt $pos) then map:put($r,"min",$mid) else map:put($r,"max",$mid -1) }, fn($r){$r?max eq $r?min} )?max return doci:line( number:= $line, text:= $doci?lines[$line], from:= $doci?starts[$line], to:= -1 ) };