1 module served.workers.rename_listener;
2 
3 import std.algorithm;
4 import std.array;
5 import std.datetime;
6 import std.experimental.logger;
7 import std.path;
8 import std.string;
9 
10 import served.extension;
11 import served.types;
12 import served.utils.translate;
13 
14 import workspaced.com.moduleman : ModulemanComponent;
15 
16 /// Helper struct which contains information about a recently opened and/or created file
17 struct FileOpenInfo
18 {
19 	/// When the file first was accessed recently
20 	SysTime at;
21 	/// The uri of the file
22 	DocumentUri uri;
23 	/// Whether the file has only been created, opened or both
24 	bool created, opened;
25 
26 	/// Returns: true when the FileOpenInfo should no longer be used (after 5 seconds)
27 	bool expired(SysTime now) const
28 	{
29 		return at == SysTime.init || (now - at) > 5.seconds;
30 	}
31 
32 	/// Sets created to true and triggers onFullyCreate if it is also opened
33 	void setCreated()
34 	{
35 		created = true;
36 		if (opened)
37 			onFullyCreate(uri);
38 	}
39 
40 	/// Sets opened to true and triggers onFullyCreate if it is also created
41 	void setOpened()
42 	{
43 		opened = true;
44 		if (created)
45 			onFullyCreate(uri);
46 	}
47 }
48 
49 /// Helper struct to keep track of recently opened & created files
50 struct RecentFiles
51 {
52 	/// the last 8 files
53 	FileOpenInfo[8] infos;
54 
55 	/// Returns a reference to some initialized FileOpenInfo with this uri
56 	ref FileOpenInfo get(DocumentUri uri) return
57 	{
58 		auto now = Clock.currTime;
59 
60 		// find existing one
61 		foreach (ref info; infos)
62 		{
63 			if (info.uri == uri)
64 			{
65 				if (info.expired(now))
66 				{
67 					info.created = false;
68 					info.opened = false;
69 				}
70 				return info;
71 			}
72 		}
73 
74 		// replace old one
75 		size_t min;
76 		SysTime minTime = now;
77 		foreach (i, ref info; infos)
78 		{
79 			if (info.at < minTime)
80 			{
81 				minTime = info.at;
82 				min = i;
83 			}
84 		}
85 
86 		infos[min].at = now;
87 		infos[min].created = infos[min].opened = false;
88 		infos[min].uri = uri;
89 		return infos[min];
90 	}
91 }
92 
93 package __gshared RecentFiles recentFiles;
94 
95 @protocolNotification("workspace/didChangeWatchedFiles")
96 void markRecentlyChangedFile(DidChangeWatchedFilesParams params)
97 {
98 	foreach (change; params.changes)
99 		if (change.type == FileChangeType.created)
100 			markRecentFileCreated(change.uri);
101 }
102 
103 void markRecentFileCreated(DocumentUri uri)
104 {
105 	recentFiles.get(uri).setCreated();
106 }
107 
108 @protocolNotification("textDocument/didOpen")
109 void markRecentFileOpened(DidOpenTextDocumentParams params)
110 {
111 	recentFiles.get(params.textDocument.uri).setOpened();
112 }
113 
114 /// Called when a file has been created or renamed and opened within a short time frame by the user
115 /// Indicating it was created to be edited in the IDE
116 void onFullyCreate(DocumentUri uri)
117 {
118 	trace("handle file creation/rename for ", uri);
119 	if (uri.endsWith(".d"))
120 		return onFullyCreateDSource(uri);
121 
122 	auto file = baseName(uri);
123 	if (file == "dub.json")
124 		onFullyCreateDubJson(uri);
125 	else if (file == "dub.sdl")
126 		onFullyCreateDubSdl(uri);
127 }
128 
129 void onFullyCreateDSource(DocumentUri uri)
130 {
131 	auto fileConfig = config(uri);
132 	if (!fileConfig.d.generateModuleNames)
133 		return;
134 
135 	string workspace = workspaceRootFor(uri);
136 	auto document = documents[uri];
137 	// Sending applyEdit so it is undoable
138 	auto patches = backend.get!ModulemanComponent(workspace)
139 		.normalizeModules(uri.uriToFile, document.rawText);
140 	if (patches.length)
141 	{
142 		WorkspaceEdit edit;
143 		edit.changes[uri] = patches.map!(a => TextEdit(TextRange(document.bytesToPosition(a.range[0]),
144 				document.bytesToPosition(a.range[1])), a.content)).array;
145 		ApplyWorkspaceEditParams params = { edit: edit };
146 		rpc.sendMethod("workspace/applyEdit", params);
147 		rpc.window.showInformationMessage(translate!"d.served.moduleNameAutoUpdated");
148 	}
149 }
150 
151 void onFullyCreateDubJson(DocumentUri uri)
152 {
153 	auto document = documents[uri];
154 	if (document.rawText.strip.length == 0)
155 	{
156 		string packageName = determineDubPackageName(uri.uriToFile.dirName);
157 		WorkspaceEdit edit;
158 		edit.changes[uri] = [
159 			TextEdit(TextRange(0, 0, 0, 0), "{\n\t\"name\": \"" ~ packageName ~ "\"\n}")
160 		];
161 		ApplyWorkspaceEditParams params = { edit: edit };
162 		rpc.sendMethod("workspace/applyEdit", params);
163 	}
164 }
165 
166 void onFullyCreateDubSdl(DocumentUri uri)
167 {
168 	auto document = documents[uri];
169 	if (document.rawText.strip.length == 0)
170 	{
171 		string packageName = determineDubPackageName(uri.uriToFile.dirName);
172 		WorkspaceEdit edit;
173 		edit.changes[uri] = [
174 			TextEdit(TextRange(0, 0, 0, 0), `name "` ~ packageName ~ `"` ~ '\n')
175 		];
176 		ApplyWorkspaceEditParams params = { edit: edit };
177 		rpc.sendMethod("workspace/applyEdit", params);
178 	}
179 }
180 
181 /// Generates a package name for a given folder path
182 string determineDubPackageName(string directory)
183 {
184 	import std.ascii : toLower, isUpper, isAlphaNum;
185 
186 	auto name = baseName(directory);
187 	if (!name.length)
188 		return "";
189 
190 	auto ret = appender!string;
191 	ret.put(toLower(name[0]));
192 	bool wasUpper = name[0].isUpper;
193 	bool wasDash = false;
194 	foreach (char c; name[1 .. $])
195 	{
196 		if (!isAlphaNum(c) && c != '_')
197 			c = '-';
198 
199 		if (wasDash && c == '-')
200 			continue;
201 		wasDash = c == '-';
202 
203 		if (c.isUpper)
204 		{
205 			if (!wasUpper)
206 			{
207 				ret.put('-');
208 				ret.put(c.toLower);
209 			}
210 			wasUpper = true;
211 		}
212 		else
213 		{
214 			wasUpper = false;
215 			ret.put(c);
216 		}
217 	}
218 	auto packageName = ret.data;
219 	while (packageName.startsWith("-"))
220 		packageName = packageName[1 .. $];
221 	while (packageName.endsWith("-"))
222 		packageName = packageName[0 .. $ - 1];
223 	return packageName;
224 }