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 : Position, Range, TextDocumentIdentifier,
27         TextDocumentItem, VersionedTextDocumentIdentifier;
28     import dls.protocol.interfaces : TextDocumentContentChangeEvent;
29     import std.json : JSONValue;
30 
31     private static Document[string] _documents;
32     private wstring[] _lines;
33     private JSONValue _version;
34 
35     @property static auto uris()
36     {
37         import std.algorithm : map;
38 
39         return _documents.keys.map!(path => Uri.fromPath(path));
40     }
41 
42     static Document get(in Uri uri)
43     {
44         import std.file : readText;
45 
46         return uri.path in _documents ? _documents[uri.path] : new Document(readText(uri.path));
47     }
48 
49     static void open(in TextDocumentItem textDocument)
50     {
51         auto uri = new Uri(textDocument.uri);
52 
53         if (uri.path !in _documents)
54         {
55             _documents[uri.path] = new Document(textDocument.text);
56             _documents[uri.path]._version = textDocument.version_;
57         }
58     }
59 
60     static void close(in TextDocumentIdentifier textDocument)
61     {
62         auto uri = new Uri(textDocument.uri);
63 
64         if (uri.path in _documents)
65         {
66             _documents.remove(uri.path);
67         }
68     }
69 
70     static void change(in VersionedTextDocumentIdentifier textDocument,
71             TextDocumentContentChangeEvent[] events)
72     {
73         auto uri = new Uri(textDocument.uri);
74 
75         if (uri.path in _documents)
76         {
77             _documents[uri.path].change(events);
78             _documents[uri.path]._version = textDocument.version_;
79         }
80     }
81 
82     @property const(wstring[]) lines() const
83     {
84         return _lines;
85     }
86 
87     @property JSONValue version_() const
88     {
89         return _version;
90     }
91 
92     private this(in string text)
93     {
94         _lines = getText(text);
95     }
96 
97     override string toString() const
98     {
99         import std.range : join;
100         import std.utf : toUTF8;
101 
102         return _lines.join().toUTF8();
103     }
104 
105     bool validatePosition(in Position position) const
106     {
107         return position.line < _lines.length && position.character <= _lines[position.line].length;
108     }
109 
110     size_t byteAtPosition(in Position position) const
111     {
112         import std.algorithm : reduce;
113         import std.range : iota;
114         import std.utf : codeLength;
115 
116         if (position.line >= _lines.length)
117         {
118             return 0;
119         }
120 
121         const linesBytes = reduce!((s, i) => s + codeLength!char(_lines[i]))(cast(size_t) 0,
122                 iota(position.line));
123 
124         if (position.character >= _lines[position.line].length)
125         {
126             return 0;
127         }
128 
129         const characterBytes = codeLength!char(_lines[position.line][0 .. position.character]);
130         return linesBytes + characterBytes;
131     }
132 
133     Position positionAtByte(size_t bytePosition) const
134     {
135         import std.algorithm : min;
136         import std.utf : codeLength, toUCSindex;
137 
138         size_t i;
139         size_t bytes;
140 
141         while (bytes <= bytePosition && i < _lines.length)
142         {
143             bytes += codeLength!char(_lines[i]);
144             ++i;
145         }
146 
147         const lineNumber = i - 1;
148         const line = _lines[lineNumber];
149         bytes -= codeLength!char(line);
150         const columnNumber = toUCSindex(line, min(bytePosition - bytes, line.length));
151         return new Position(lineNumber, columnNumber);
152     }
153 
154     Range wordRangeAtByte(size_t bytePosition) const
155     {
156         return wordRangeAtPosition(positionAtByte(bytePosition));
157     }
158 
159     Range wordRangeAtPosition(in Position position) const
160     {
161         import std.algorithm : min;
162         import std.regex : matchAll, regex;
163 
164         const line = _lines[min(position.line, $ - 1)];
165         const startCharacter = min(position.character, line.length);
166         auto word = matchAll(line[startCharacter .. $], regex(`\w+|.`w));
167         return new Range(new Position(position.line, startCharacter),
168                 new Position(position.line, startCharacter + (word ? word.hit.length : 0)));
169     }
170 
171     Range wordRangeAtLineAndByte(size_t lineNumber, size_t bytePosition) const
172     {
173         import std.utf : toUCSindex;
174 
175         return wordRangeAtPosition(new Position(lineNumber,
176                 toUCSindex(_lines[lineNumber], bytePosition)));
177     }
178 
179     private void change(in TextDocumentContentChangeEvent[] events)
180     {
181         foreach (event; events)
182         {
183             if (event.range.isNull)
184             {
185                 _lines = getText(event.text);
186             }
187             else
188             {
189                 with (event.range)
190                 {
191                     auto linesBefore = _lines[0 .. start.line];
192                     auto linesAfter = _lines[end.line + 1 .. $];
193 
194                     auto lineStart = _lines[start.line][0 .. start.character];
195                     auto lineEnd = _lines[end.line][end.character .. $];
196 
197                     auto newLines = getText(event.text);
198 
199                     if (newLines.length)
200                     {
201                         newLines[0] = lineStart ~ newLines[0];
202                         newLines[$ - 1] = newLines[$ - 1] ~ lineEnd;
203                     }
204                     else
205                     {
206                         newLines = [lineStart ~ lineEnd];
207                     }
208 
209                     _lines = linesBefore ~ newLines ~ linesAfter;
210                 }
211             }
212         }
213     }
214 
215     private wstring[] getText(in string text) const
216     {
217         import std.algorithm : endsWith;
218         import std.array : replaceFirst;
219         import std.encoding : getBOM;
220         import std..string : splitLines;
221         import std.typecons : Yes;
222         import std.utf : toUTF16;
223 
224         auto lines = text.replaceFirst(cast(string) getBOM(cast(ubyte[]) text)
225                 .sequence, "").toUTF16().splitLines(Yes.keepTerminator);
226 
227         if (!lines.length || lines[$ - 1].endsWith('\r', '\n'))
228         {
229             lines ~= "";
230         }
231 
232         return lines;
233     }
234 }