1 module dls.util.document;
2 
3 import dls.util.uri : Uri;
4 
5 class Document
6 {
7     import dls.protocol.definitions : Position, Range, TextDocumentIdentifier,
8         TextDocumentItem, VersionedTextDocumentIdentifier;
9     import dls.protocol.interfaces : TextDocumentContentChangeEvent;
10     import std.utf : codeLength, toUTF8;
11 
12     private static Document[string] _documents;
13     private wstring[] _lines;
14 
15     static Document opIndex(in Uri uri)
16     {
17         return uri.path in _documents ? _documents[uri.path] : null;
18     }
19 
20     @property static auto uris()
21     {
22         import std.algorithm : map;
23 
24         return _documents.keys.map!(path => Uri.fromPath(path));
25     }
26 
27     static void open(in TextDocumentItem textDocument)
28     {
29         auto path = Uri.getPath(textDocument.uri);
30 
31         if (path in _documents)
32         {
33             _documents.remove(path);
34         }
35 
36         _documents[path] = new Document(textDocument);
37     }
38 
39     static void close(in TextDocumentIdentifier textDocument)
40     {
41         auto path = Uri.getPath(textDocument.uri);
42 
43         if (path in _documents)
44         {
45             _documents.remove(path);
46         }
47     }
48 
49     static void change(in VersionedTextDocumentIdentifier textDocument,
50             TextDocumentContentChangeEvent[] events)
51     {
52         auto path = Uri.getPath(textDocument.uri);
53 
54         if (path in _documents)
55         {
56             _documents[path].change(events);
57         }
58     }
59 
60     @property const(wstring[]) lines() const
61     {
62         return _lines;
63     }
64 
65     this(in TextDocumentItem textDocument)
66     {
67         _lines = getText(textDocument.text);
68     }
69 
70     override string toString() const
71     {
72         import std.range : join;
73 
74         return _lines.join().toUTF8();
75     }
76 
77     size_t byteAtPosition(in Position position)
78     {
79         import std.algorithm : reduce;
80         import std.range : iota;
81 
82         const linesBytes = reduce!((s, i) => s + codeLength!char(_lines[i]))(cast(size_t) 0,
83                 iota(position.line));
84         const characterBytes = codeLength!char(_lines[position.line][0 .. position.character]);
85         return linesBytes + characterBytes;
86     }
87 
88     Range wordRangeAtByte(size_t bytePosition)
89     {
90         import std.algorithm : min;
91 
92         size_t i;
93         size_t bytes;
94 
95         while (bytes <= bytePosition && i < _lines.length)
96         {
97             bytes += codeLength!char(_lines[i]);
98             ++i;
99         }
100 
101         const lineNumber = i - 1;
102         const line = _lines[lineNumber];
103         bytes -= codeLength!char(line);
104         return wordRangeAtLineAndByte(lineNumber, min(bytePosition - bytes, line.length));
105     }
106 
107     Range wordRangeAtLineAndByte(size_t lineNumber, size_t bytePosition)
108     {
109         import std.regex : matchAll, regex;
110         import std.utf : UTFException, validate;
111 
112         const line = _lines[lineNumber];
113         size_t startCharacter;
114         const lineSlice = line.toUTF8()[0 .. bytePosition];
115 
116         try
117         {
118             validate(lineSlice);
119             startCharacter = codeLength!wchar(lineSlice);
120         }
121         catch (UTFException e)
122         {
123             // TODO: properly use document buffers instead of on-disc files
124         }
125 
126         auto word = matchAll(line[startCharacter .. $], regex(`\w+|.`w));
127         return new Range(new Position(lineNumber, startCharacter),
128                 new Position(lineNumber, startCharacter + (word ? word.hit.length : 0)));
129     }
130 
131     private void change(in TextDocumentContentChangeEvent[] events)
132     {
133         foreach (event; events)
134         {
135             if (event.range.isNull)
136             {
137                 _lines = getText(event.text);
138             }
139             else
140             {
141                 with (event.range)
142                 {
143                     auto linesBefore = _lines[0 .. start.line];
144                     auto linesAfter = _lines[end.line + 1 .. $];
145 
146                     auto lineStart = _lines[start.line][0 .. start.character];
147                     auto lineEnd = _lines[end.line][end.character .. $];
148 
149                     auto newLines = getText(event.text);
150 
151                     if (newLines.length)
152                     {
153                         newLines[0] = lineStart ~ newLines[0];
154                         newLines[$ - 1] = newLines[$ - 1] ~ lineEnd;
155                     }
156                     else
157                     {
158                         newLines = [lineStart ~ lineEnd];
159                     }
160 
161                     _lines = linesBefore ~ newLines ~ linesAfter;
162                 }
163             }
164         }
165     }
166 
167     private wstring[] getText(in string text) const
168     {
169         import std.algorithm : endsWith;
170         import std.array : array, replaceFirst;
171         import std.encoding : getBOM;
172         import std.string : splitLines;
173         import std.typecons : Yes;
174         import std.utf : toUTF16;
175 
176         auto lines = text.replaceFirst(cast(string) getBOM(cast(ubyte[]) text)
177                 .sequence, "").toUTF16().splitLines(Yes.keepTerminator);
178 
179         if (!lines.length || lines[$ - 1].endsWith('\r', '\n'))
180         {
181             lines ~= "";
182         }
183 
184         return lines;
185     }
186 }