1 /*
2  *Copyright (C) 2018 Laurent Tréguier
3  *
4  *This file is part of DLS.
5  *
6  *DLS is free software: you can redistribute it and/or modify
7  *it under the terms of the GNU General Public License as published by
8  *the Free Software Foundation, either version 3 of the License, or
9  *(at your option) any later version.
10  *
11  *DLS is distributed in the hope that it will be useful,
12  *but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *GNU General Public License for more details.
15  *
16  *You should have received a copy of the GNU General Public License
17  *along with DLS.  If not, see <http://www.gnu.org/licenses/>.
18  *
19  */
20 
21 module dls.util.document;
22 
23 class Document
24 {
25     import dls.util.uri : Uri;
26     import dls.protocol.definitions : DocumentUri, Position, Range,
27         TextDocumentIdentifier, TextDocumentItem, VersionedTextDocumentIdentifier;
28     import dls.protocol.interfaces : TextDocumentContentChangeEvent;
29     import std.json : JSONValue;
30 
31     private static Document[string] _documents;
32     private DocumentUri _uri;
33     private wstring[] _lines;
34     private JSONValue _version;
35 
36     @property static auto uris()
37     {
38         import std.algorithm : map;
39 
40         return _documents.byValue.map!(doc => new Uri(doc._uri));
41     }
42 
43     static Document get(const Uri uri)
44     {
45         import std.file : readText;
46 
47         return uri.path in _documents ? _documents[uri.path] : new Document(uri, readText(uri.path));
48     }
49 
50     static bool open(const TextDocumentItem textDocument)
51     {
52         auto uri = new Uri(textDocument.uri);
53 
54         if (uri.path !in _documents)
55         {
56             _documents[uri.path] = new Document(uri, textDocument.text);
57             _documents[uri.path]._version = textDocument.version_;
58             return true;
59         }
60         else
61         {
62             return false;
63         }
64     }
65 
66     static bool close(const TextDocumentIdentifier textDocument)
67     {
68         auto uri = new Uri(textDocument.uri);
69 
70         if (uri.path in _documents)
71         {
72             _documents.remove(uri.path);
73             return true;
74         }
75         else
76         {
77             return false;
78         }
79     }
80 
81     static bool change(const VersionedTextDocumentIdentifier textDocument,
82             TextDocumentContentChangeEvent[] events)
83     {
84         auto uri = new Uri(textDocument.uri);
85 
86         if (uri.path in _documents)
87         {
88             _documents[uri.path].change(events);
89             _documents[uri.path]._version = textDocument.version_;
90             return true;
91         }
92         else
93         {
94             return false;
95         }
96     }
97 
98     @property const(wstring[]) lines() const
99     {
100         return _lines;
101     }
102 
103     @property JSONValue version_() const
104     {
105         return _version;
106     }
107 
108     private this(const Uri uri, const string text)
109     {
110         _uri = uri;
111         _lines = getText(text);
112     }
113 
114     override string toString() const
115     {
116         import std.range : join;
117         import std.utf : toUTF8;
118 
119         return _lines.join().toUTF8();
120     }
121 
122     void validatePosition(const Position position) const
123     {
124         import dls.protocol.errors : InvalidParamsException;
125         import std.format : format;
126 
127         if (position.line >= _lines.length || position.character > _lines[position.line].length)
128         {
129             throw new InvalidParamsException(format!"invalid position: %s %s,%s"(_uri,
130                     position.line, position.character));
131         }
132     }
133 
134     size_t byteAtPosition(const Position position) const
135     {
136         import std.algorithm : min;
137         import std.utf : codeLength;
138 
139         size_t result;
140 
141         foreach (i, ref line; _lines)
142         {
143             if (i < position.line)
144             {
145                 result += codeLength!char(line);
146             }
147             else
148             {
149                 result += codeLength!char(line[0 .. min(position.character, $)]);
150                 break;
151             }
152         }
153 
154         return result;
155     }
156 
157     Position positionAtByte(size_t bytePosition) const
158     {
159         import std.algorithm : min;
160         import std.utf : codeLength, toUTF8;
161 
162         size_t i;
163         size_t bytes;
164 
165         while (bytes <= bytePosition && i < _lines.length)
166         {
167             bytes += codeLength!char(_lines[i]);
168             ++i;
169         }
170 
171         immutable lineNumber = minusOne(i);
172         immutable line = _lines[lineNumber];
173         bytes -= codeLength!char(line);
174         immutable columnByte = min(bytePosition - bytes, line.length);
175         immutable columnNumber = codeLength!wchar(line.toUTF8()[0 .. columnByte]);
176         return new Position(lineNumber, columnNumber);
177     }
178 
179     Range wordRangeAtPosition(const Position position) const
180     {
181         import std.algorithm : min;
182 
183         immutable line = _lines[min(position.line, $ - 1)];
184         immutable middleIndex = min(position.character, line.length);
185         size_t startIndex = middleIndex;
186         size_t endIndex = middleIndex;
187 
188         static bool isIdentifierChar(wchar c)
189         {
190             import std.ascii : isPunctuation, isWhite;
191 
192             return !isWhite(c) && (!isPunctuation(c) || c == '_');
193         }
194 
195         while (startIndex > 0 && isIdentifierChar(line[minusOne(startIndex)]))
196         {
197             --startIndex;
198         }
199 
200         while (endIndex < line.length && isIdentifierChar(line[endIndex]))
201         {
202             ++endIndex;
203         }
204 
205         return new Range(new Position(position.line, startIndex),
206                 new Position(position.line, endIndex));
207     }
208 
209     Range wordRangeAtLineAndByte(size_t lineNumber, size_t bytePosition) const
210     {
211         import std.algorithm : min;
212         import std.utf : codeLength, toUTF8;
213 
214         return wordRangeAtPosition(new Position(lineNumber,
215                 codeLength!wchar(_lines[lineNumber].toUTF8()[0 .. min(bytePosition, $)])));
216     }
217 
218     Range wordRangeAtByte(size_t bytePosition) const
219     {
220         return wordRangeAtPosition(positionAtByte(bytePosition));
221     }
222 
223     private void change(const TextDocumentContentChangeEvent[] events)
224     {
225         foreach (event; events)
226         {
227             if (event.range.isNull)
228             {
229                 _lines = getText(event.text);
230             }
231             else
232             {
233                 with (event.range.get)
234                 {
235                     auto linesBefore = _lines[0 .. start.line];
236                     auto linesAfter = _lines[end.line + 1 .. $];
237 
238                     auto lineStart = _lines[start.line][0 .. start.character];
239                     auto lineEnd = _lines[end.line][end.character .. $];
240 
241                     auto newLines = getText(event.text);
242 
243                     if (newLines.length)
244                     {
245                         newLines[0] = lineStart ~ newLines[0];
246                         newLines[$ - 1] = newLines[$ - 1] ~ lineEnd;
247                     }
248                     else
249                     {
250                         newLines = [lineStart ~ lineEnd];
251                     }
252 
253                     _lines = linesBefore ~ newLines ~ linesAfter;
254                 }
255             }
256         }
257     }
258 
259     private wstring[] getText(const string text) const
260     {
261         import std.algorithm : endsWith;
262         import std.array : replaceFirst;
263         import std.encoding : getBOM;
264         import std..string : splitLines;
265         import std.typecons : Yes;
266         import std.utf : toUTF16;
267 
268         auto lines = text.replaceFirst(cast(string) getBOM(cast(ubyte[]) text)
269                 .sequence, "").toUTF16().splitLines(Yes.keepTerminator);
270 
271         if (!lines.length || lines[$ - 1].endsWith('\r', '\n'))
272         {
273             lines ~= "";
274         }
275 
276         return lines;
277     }
278 }
279 
280 size_t minusOne(size_t i)
281 {
282     return i > 0 ? i - 1 : 0;
283 }